From 74f63df43a01b3deeca261152e3f90a3d0776dbb Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 5 May 2023 08:45:34 +0100 Subject: [PATCH 001/111] Fix typo in ReShade removal notice (#195) mfw https://twitter.com/boxerpizza/status/1650825767553495041 came back to slap me in the face --- Bloxstrap/Bootstrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 9ecfb0d..5b915eb 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -849,7 +849,7 @@ namespace Bloxstrap if (File.Exists(injectorLocation)) { App.ShowMessageBox( - "Roblox has now completeted rollout of the new client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been removed from Bloxstrap.\n\n"+ + "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", MessageBoxImage.Warning ); From 1a4ebad370fa09e81e24a9d590da7df0c2f3c03b Mon Sep 17 00:00:00 2001 From: fxe <64731916+fxeP1@users.noreply.github.com> Date: Sat, 6 May 2023 09:47:16 +0100 Subject: [PATCH 002/111] Implement Direct3D10 rendering mode. --- Bloxstrap/Singletons/FastFlagManager.cs | 12 +++++++++--- Bloxstrap/ViewModels/FastFlagsViewModel.cs | 6 +++++- Bloxstrap/Views/Pages/FastFlagsPage.xaml | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index d158752..4a5b26f 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -20,6 +20,7 @@ namespace Bloxstrap.Singletons { { "Automatic", "" }, { "Direct3D 11", "FFlagDebugGraphicsPreferD3D11" }, + { "Direct3D 10", "FFlagDebugGraphicsPreferD3D11FL10" }, { "Vulkan", "FFlagDebugGraphicsPreferVulkan" }, { "OpenGL", "FFlagDebugGraphicsPreferOpenGL" } }; @@ -120,9 +121,14 @@ namespace Bloxstrap.Singletons if (GetValue("DFIntTaskSchedulerTargetFps") is null) SetValue("DFIntTaskSchedulerTargetFps", 9999); - // reshade / exclusive fullscreen requires direct3d 11 to work - if (GetValue(RenderingModes["Direct3D 11"]) != "True" && App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False") - SetRenderingMode("Direct3D 11"); + // exclusive fullscreen requires direct3d 10/11 to work + if (App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False") + { + if (!(App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11") == "True" || App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11FL10") == "True")) + { + SetRenderingMode("Direct3D 11"); + } + } } public override void Save() diff --git a/Bloxstrap/ViewModels/FastFlagsViewModel.cs b/Bloxstrap/ViewModels/FastFlagsViewModel.cs index a60c5a4..6531153 100644 --- a/Bloxstrap/ViewModels/FastFlagsViewModel.cs +++ b/Bloxstrap/ViewModels/FastFlagsViewModel.cs @@ -53,7 +53,11 @@ namespace Bloxstrap.ViewModels if (value) { - App.FastFlags.SetRenderingMode("Direct3D 11"); + if (!(App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11") == "True" || App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11FL10") == "True")) + { + App.FastFlags.SetRenderingMode("Direct3D 11"); + } + OnPropertyChanged(nameof(SelectedRenderingMode)); } } diff --git a/Bloxstrap/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/Views/Pages/FastFlagsPage.xaml index 1f435dc..50deec3 100644 --- a/Bloxstrap/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/Views/Pages/FastFlagsPage.xaml @@ -65,7 +65,7 @@ - + From 5b7d52581c4e8332c81cc0735c744b56d05fd1c1 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 7 May 2023 07:52:45 +0100 Subject: [PATCH 003/111] Minor wording change --- Bloxstrap/Views/Pages/FastFlagsPage.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/Views/Pages/FastFlagsPage.xaml index 50deec3..1d9ff6a 100644 --- a/Bloxstrap/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/Views/Pages/FastFlagsPage.xaml @@ -65,7 +65,7 @@ - + From 39d919811f1ff1914f5cedd370cda4ca6662f0d3 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 9 May 2023 12:21:56 +0100 Subject: [PATCH 004/111] Add support for game message communication (#183) game scripts can print a client message to send messages to bloxstrap, right now only for setting the presence status message is a json string formatted with the properties "command" and "data" that's prefixed with the string "[SendBloxstrapMessage]" example: print('[SendBloxstrapMessage] {"command":"SetPresenceStatus", "data":"hi"}') --- Bloxstrap/Integrations/DiscordRichPresence.cs | 97 ++++++++++++++++--- Bloxstrap/Models/GameMessage.cs | 13 +++ Bloxstrap/RobloxActivity.cs | 36 +++++++ 3 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 Bloxstrap/Models/GameMessage.cs diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs index 69c10e4..12a733d 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using DiscordRPC; using Bloxstrap.Models.RobloxApi; +using Bloxstrap.Models; namespace Bloxstrap.Integrations { @@ -14,6 +15,8 @@ namespace Bloxstrap.Integrations private readonly DiscordRpcClient _rpcClient = new("1005469189907173486"); private readonly RobloxActivity _activityWatcher; + private RichPresence? _currentPresence; + private string? _initialStatus; private long _currentUniverseId; private DateTime? _timeStartedUniverse; @@ -21,8 +24,9 @@ namespace Bloxstrap.Integrations { _activityWatcher = activityWatcher; - _activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetPresence()); - _activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetPresence()); + _activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame()); + _activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame()); + _activityWatcher.OnGameMessage += (_, message) => OnGameMessage(message); _rpcClient.OnReady += (_, e) => App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})"); @@ -46,28 +50,77 @@ namespace Bloxstrap.Integrations _rpcClient.Initialize(); } - public async Task SetPresence() + public void OnGameMessage(GameMessage message) + { + if (message.Command == "SetPresenceStatus") + SetStatus(message.Data); + } + + public void SetStatus(string status) + { + App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Setting status to '{status}'"); + + if (_currentPresence is null) + { + App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Presence is not set, aborting"); + return; + } + + if (status.Length > 128) + { + App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status cannot be longer than 128 characters, aborting"); + return; + } + + if (_initialStatus is null) + _initialStatus = _currentPresence.State; + + string finalStatus; + + if (string.IsNullOrEmpty(status)) + { + App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status is empty, reverting to initial status"); + finalStatus = _initialStatus; + } + else + { + finalStatus = status; + } + + if (_currentPresence.State == finalStatus) + { + App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status is unchanged, aborting"); + return; + } + + _currentPresence.State = finalStatus; + UpdatePresence(); + } + + public async Task SetCurrentGame() { if (!_activityWatcher.ActivityInGame) { - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Clearing presence"); - _rpcClient.ClearPresence(); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Not in game, clearing presence"); + _currentPresence = null; + _initialStatus = null; + UpdatePresence(); return true; } string icon = "roblox"; - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Setting presence for Place ID {_activityWatcher.ActivityPlaceId}"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Setting presence for Place ID {_activityWatcher.ActivityPlaceId}"); var universeIdResponse = await Utilities.GetJson($"https://apis.roblox.com/universes/v1/places/{_activityWatcher.ActivityPlaceId}/universe"); if (universeIdResponse is null) { - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe ID!"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe ID!"); return false; } long universeId = universeIdResponse.UniverseId; - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Got Universe ID as {universeId}"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe ID as {universeId}"); // preserve time spent playing if we're teleporting between places in the same universe if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId) @@ -79,22 +132,22 @@ namespace Bloxstrap.Integrations var gameDetailResponse = await Utilities.GetJson>($"https://games.roblox.com/v1/games?universeIds={universeId}"); if (gameDetailResponse is null || !gameDetailResponse.Data.Any()) { - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe info!"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe info!"); return false; } GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0]; - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Got Universe details"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe details"); var universeThumbnailResponse = await Utilities.GetJson>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any()) { - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe thumbnail info!"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe thumbnail info!"); } else { icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl; - App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Got Universe thumbnail as {icon}"); + App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe thumbnail as {icon}"); } List + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Bloxstrap/Dialogs/HyperionDialog.xaml.cs b/Bloxstrap/Dialogs/HyperionDialog.xaml.cs new file mode 100644 index 0000000..a8514fc --- /dev/null +++ b/Bloxstrap/Dialogs/HyperionDialog.xaml.cs @@ -0,0 +1,100 @@ +using System; +using System.Windows; +using System.Windows.Forms; + +using Bloxstrap.Enums; +using Bloxstrap.Extensions; +using Bloxstrap.ViewModels; + +namespace Bloxstrap.Dialogs +{ + /// + /// Interaction logic for HyperionDialog.xaml + /// + public partial class HyperionDialog : IBootstrapperDialog + { + private readonly HyperionDialogViewModel _viewModel; + + public Bootstrapper? Bootstrapper { get; set; } + + #region UI Elements + public string Message + { + get => _viewModel.Message; + set + { + _viewModel.Message = value; + _viewModel.OnPropertyChanged(nameof(_viewModel.Message)); + } + } + + public ProgressBarStyle ProgressStyle + { + get => _viewModel.ProgressIndeterminate ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous; + set + { + _viewModel.ProgressIndeterminate = (value == ProgressBarStyle.Marquee); + _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressIndeterminate)); + } + } + + public int ProgressValue + { + get => _viewModel.ProgressValue; + set + { + _viewModel.ProgressValue = value; + _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressValue)); + } + } + + public bool CancelEnabled + { + get => _viewModel.CancelButtonVisibility == Visibility.Visible; + set + { + _viewModel.CancelButtonVisibility = (value ? Visibility.Visible : Visibility.Collapsed); + _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); + } + } + #endregion + + public HyperionDialog() + { + _viewModel = new HyperionDialogViewModel(this); + DataContext = _viewModel; + InitializeComponent(); + } + + #region IBootstrapperDialog Methods + // Referencing FluentDialog + public void ShowBootstrapper() => this.ShowDialog(); + + public void CloseBootstrapper() => Dispatcher.BeginInvoke(this.Close); + + public void ShowSuccess(string message) + { + App.ShowMessageBox(message, MessageBoxImage.Information); + App.Terminate(); + } + + public void ShowError(string message) + { + App.ShowMessageBox($"An error occurred while starting Roblox\n\nDetails: {message}", MessageBoxImage.Error); + App.Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); + } + + public void PromptShutdown() + { + MessageBoxResult result = App.ShowMessageBox( + "Roblox is currently running, but needs to close. Would you like close Roblox now?", + MessageBoxImage.Information, + MessageBoxButton.OKCancel + ); + + if (result != MessageBoxResult.OK) + Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT); + } + #endregion + } +} diff --git a/Bloxstrap/Resources/CancelButtonSmall.png b/Bloxstrap/Resources/CancelButtonSmall.png new file mode 100644 index 0000000000000000000000000000000000000000..77e6a393d41fade2c9e02a5c48e80139c5f1cc62 GIT binary patch literal 2663 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 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + private readonly IBootstrapperDialog _dialog; + + public ICommand CancelInstallCommand => new RelayCommand(CancelInstall); + + public string Title => App.Settings.Prop.BootstrapperTitle; + public string Message { get; set; } = "Please wait..."; + public bool ProgressIndeterminate { get; set; } = true; + public int ProgressValue { get; set; } = 0; + + public Visibility CancelButtonVisibility { get; set; } = Visibility.Collapsed; + + public string Version => $"Bloxstrap v{App.Version}"; + + public HyperionDialogViewModel(IBootstrapperDialog dialog) + { + _dialog = dialog; + } + + private void CancelInstall() + { + _dialog.Bootstrapper?.CancelInstall(); + _dialog.CloseBootstrapper(); + } + } +} From 703181b3360a9bf7408cd3207be86dd6fb0939d8 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Tue, 9 May 2023 21:46:15 +0800 Subject: [PATCH 007/111] Fix styling --- Bloxstrap/Dialogs/HyperionDialog.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/Dialogs/HyperionDialog.xaml b/Bloxstrap/Dialogs/HyperionDialog.xaml index bc271a4..41b86c1 100644 --- a/Bloxstrap/Dialogs/HyperionDialog.xaml +++ b/Bloxstrap/Dialogs/HyperionDialog.xaml @@ -20,7 +20,7 @@ - + @@ -28,8 +28,8 @@ - - + + From 26a7dfe5af81026ed274ec6d7558dfef26345ba4 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 10 May 2023 13:41:36 +0100 Subject: [PATCH 008/111] Make Hyperion dialog inherit from Fluent dialog --- Bloxstrap/ViewModels/FluentDialogViewModel.cs | 10 ++--- .../ViewModels/HyperionDialogViewModel.cs | 37 ++----------------- 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/Bloxstrap/ViewModels/FluentDialogViewModel.cs b/Bloxstrap/ViewModels/FluentDialogViewModel.cs index fc81ba1..881c4d4 100644 --- a/Bloxstrap/ViewModels/FluentDialogViewModel.cs +++ b/Bloxstrap/ViewModels/FluentDialogViewModel.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel; using System.Windows; using System.Windows.Input; using System.Windows.Media; + using CommunityToolkit.Mvvm.Input; + using Bloxstrap.Dialogs; -using Bloxstrap.Enums; using Bloxstrap.Extensions; namespace Bloxstrap.ViewModels diff --git a/Bloxstrap/ViewModels/HyperionDialogViewModel.cs b/Bloxstrap/ViewModels/HyperionDialogViewModel.cs index 6a5c452..c8415f2 100644 --- a/Bloxstrap/ViewModels/HyperionDialogViewModel.cs +++ b/Bloxstrap/ViewModels/HyperionDialogViewModel.cs @@ -1,45 +1,14 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Input; -using System.Windows.Media; -using CommunityToolkit.Mvvm.Input; +using System.ComponentModel; using Bloxstrap.Dialogs; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; namespace Bloxstrap.ViewModels { - public class HyperionDialogViewModel : INotifyPropertyChanged + public class HyperionDialogViewModel : FluentDialogViewModel, INotifyPropertyChanged { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private readonly IBootstrapperDialog _dialog; - - public ICommand CancelInstallCommand => new RelayCommand(CancelInstall); - - public string Title => App.Settings.Prop.BootstrapperTitle; - public string Message { get; set; } = "Please wait..."; - public bool ProgressIndeterminate { get; set; } = true; - public int ProgressValue { get; set; } = 0; - - public Visibility CancelButtonVisibility { get; set; } = Visibility.Collapsed; - public string Version => $"Bloxstrap v{App.Version}"; - public HyperionDialogViewModel(IBootstrapperDialog dialog) + public HyperionDialogViewModel(IBootstrapperDialog dialog) : base(dialog) { - _dialog = dialog; - } - - private void CancelInstall() - { - _dialog.Bootstrapper?.CancelInstall(); - _dialog.CloseBootstrapper(); } } } From 3f6154cf84b2bdd8970a111cbbe2208a7813ac62 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 10 May 2023 13:44:09 +0100 Subject: [PATCH 009/111] Change ordering in Appearance menu --- Bloxstrap/ViewModels/AppearanceViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/ViewModels/AppearanceViewModel.cs b/Bloxstrap/ViewModels/AppearanceViewModel.cs index b6efcbd..7e0a469 100644 --- a/Bloxstrap/ViewModels/AppearanceViewModel.cs +++ b/Bloxstrap/ViewModels/AppearanceViewModel.cs @@ -71,11 +71,11 @@ namespace Bloxstrap.ViewModels public IReadOnlyDictionary Dialogs { get; set; } = new Dictionary() { { "Fluent", BootstrapperStyle.FluentDialog }, - { "Hyperion", BootstrapperStyle.HyperionDialog }, { "Progress (~2014)", BootstrapperStyle.ProgressDialog }, { "Legacy (2011 - 2014)", BootstrapperStyle.LegacyDialog2011 }, { "Legacy (2009 - 2011)", BootstrapperStyle.LegacyDialog2009 }, { "Vista (2009 - 2011)", BootstrapperStyle.VistaDialog }, + { "Hyperion (2022 - Fake)", BootstrapperStyle.HyperionDialog }, }; public string Dialog From 6de96bbf609ede3e40923711ee92cf4eb0c32745 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Thu, 11 May 2023 00:56:13 +0800 Subject: [PATCH 010/111] Embedding SVG data into dialog and make it theme-aware --- Bloxstrap/Bloxstrap.csproj | 2 -- Bloxstrap/Dialogs/HyperionDialog.xaml | 34 +++++++++++------- Bloxstrap/Dialogs/HyperionDialog.xaml.cs | 11 ++++++ Bloxstrap/Resources/CancelButtonSmall.png | Bin 2663 -> 0 bytes Bloxstrap/Resources/WordmarkRoblox.png | Bin 6104 -> 0 bytes .../ViewModels/HyperionDialogViewModel.cs | 9 ++++- 6 files changed, 40 insertions(+), 16 deletions(-) delete mode 100644 Bloxstrap/Resources/CancelButtonSmall.png delete mode 100644 Bloxstrap/Resources/WordmarkRoblox.png diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 16faf36..05de60f 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -14,8 +14,6 @@ - - diff --git a/Bloxstrap/Dialogs/HyperionDialog.xaml b/Bloxstrap/Dialogs/HyperionDialog.xaml index 41b86c1..f0677e1 100644 --- a/Bloxstrap/Dialogs/HyperionDialog.xaml +++ b/Bloxstrap/Dialogs/HyperionDialog.xaml @@ -12,25 +12,33 @@ WindowStartupLocation="CenterScreen" AllowsTransparency="True" Background="Transparent"> - - - - - - - + + + + + + + + + + + + - - + + + \ No newline at end of file diff --git a/Bloxstrap/Dialogs/HyperionDialog.xaml.cs b/Bloxstrap/Dialogs/HyperionDialog.xaml.cs index a8514fc..feee352 100644 --- a/Bloxstrap/Dialogs/HyperionDialog.xaml.cs +++ b/Bloxstrap/Dialogs/HyperionDialog.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Windows; using System.Windows.Forms; +using System.Windows.Media; using Bloxstrap.Enums; using Bloxstrap.Extensions; @@ -62,7 +63,17 @@ namespace Bloxstrap.Dialogs public HyperionDialog() { _viewModel = new HyperionDialogViewModel(this); + if (App.Settings.Prop.Theme.GetFinal() == Theme.Light) + { + // Matching the roblox website light theme as close as possible. + _viewModel.DialogBorder = new Thickness(1); + _viewModel.Background = new SolidColorBrush(Color.FromRgb(242, 244, 245)); + _viewModel.Foreground = new SolidColorBrush(Color.FromRgb(57, 59, 61)); + _viewModel.IconColor = new SolidColorBrush(Color.FromRgb(57, 59, 61)); + _viewModel.ProgressBarBackground = new SolidColorBrush(Color.FromRgb(189, 190, 190)); + } DataContext = _viewModel; + InitializeComponent(); } diff --git a/Bloxstrap/Resources/CancelButtonSmall.png b/Bloxstrap/Resources/CancelButtonSmall.png deleted file mode 100644 index 77e6a393d41fade2c9e02a5c48e80139c5f1cc62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2663 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 $"Bloxstrap v{App.Version}"; - + // Using dark theme for default values. + public Thickness DialogBorder { get; set; } = new Thickness(0); + public Brush Background { get; set; } = Brushes.Black; + public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(216, 216, 216)); + public Brush IconColor { get; set; } = new SolidColorBrush(Color.FromRgb(255, 255, 255)); + public Brush ProgressBarBackground { get; set; } = new SolidColorBrush(Color.FromRgb(86, 86, 86)); public HyperionDialogViewModel(IBootstrapperDialog dialog) : base(dialog) { } From 85c83af15e4a5685242a89ef45879f0cf3a2aed0 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Thu, 11 May 2023 01:15:07 +0800 Subject: [PATCH 011/111] Fix padding on close button xd --- Bloxstrap/Dialogs/HyperionDialog.xaml | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Bloxstrap/Dialogs/HyperionDialog.xaml b/Bloxstrap/Dialogs/HyperionDialog.xaml index f0677e1..75b19ce 100644 --- a/Bloxstrap/Dialogs/HyperionDialog.xaml +++ b/Bloxstrap/Dialogs/HyperionDialog.xaml @@ -15,7 +15,7 @@ - - + @@ -35,7 +35,7 @@ - + diff --git a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs index 20b8e46..d1743d5 100644 --- a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs +++ b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs @@ -8,10 +8,11 @@ namespace Bloxstrap.ViewModels public class ByfronDialogViewModel : FluentDialogViewModel, INotifyPropertyChanged { public string Version => $"Bloxstrap v{App.Version}"; + // Using dark theme for default values. public Thickness DialogBorder { get; set; } = new Thickness(0); public Brush Background { get; set; } = Brushes.Black; - public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(216, 216, 216)); + public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(235, 235, 235)); public Brush IconColor { get; set; } = new SolidColorBrush(Color.FromRgb(255, 255, 255)); public Brush ProgressBarBackground { get; set; } = new SolidColorBrush(Color.FromRgb(86, 86, 86)); public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) From 066fda920e20f5364f800b31fa0d339bb8cf6323 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Sat, 13 May 2023 10:29:21 +0800 Subject: [PATCH 015/111] More corrections to mimic the dialog --- Bloxstrap/Fonts/Rubik-VariableFont_wght.ttf | Bin 0 -> 193736 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Bloxstrap/Fonts/Rubik-VariableFont_wght.ttf diff --git a/Bloxstrap/Fonts/Rubik-VariableFont_wght.ttf b/Bloxstrap/Fonts/Rubik-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..547f06974a0a11afca9e5c305544e406a1609d72 GIT binary patch literal 193736 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 ~Rl&Bf`Ai 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 F{a;$^yfXj* literal 0 HcmV?d00001 From 7e82dc4436df49d2a07e477763916635d7349ce9 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Sat, 13 May 2023 10:31:05 +0800 Subject: [PATCH 016/111] Didn't pass the -a flag in git... --- Bloxstrap/App.xaml | 2 ++ Bloxstrap/Bloxstrap.csproj | 1 + Bloxstrap/Dialogs/ByfronDialog.xaml | 8 ++++++-- Bloxstrap/ViewModels/ByfronDialogViewModel.cs | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml index 8f4642f..3ac28c3 100644 --- a/Bloxstrap/App.xaml +++ b/Bloxstrap/App.xaml @@ -11,6 +11,8 @@ + + pack://application:,,,/Fonts/#Rubik Light diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 05de60f..60a5348 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -14,6 +14,7 @@ + diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml b/Bloxstrap/Dialogs/ByfronDialog.xaml index be0b987..30dd0ab 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml @@ -23,7 +23,7 @@ - + @@ -35,7 +35,11 @@ - + + + + + diff --git a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs index d1743d5..7609046 100644 --- a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs +++ b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs @@ -12,7 +12,7 @@ namespace Bloxstrap.ViewModels // Using dark theme for default values. public Thickness DialogBorder { get; set; } = new Thickness(0); public Brush Background { get; set; } = Brushes.Black; - public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(235, 235, 235)); + public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(239, 239, 239)); public Brush IconColor { get; set; } = new SolidColorBrush(Color.FromRgb(255, 255, 255)); public Brush ProgressBarBackground { get; set; } = new SolidColorBrush(Color.FromRgb(86, 86, 86)); public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) From 74d15c9bf38f58fc6e32cb34def670f3475aab29 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Sat, 13 May 2023 11:57:16 +0800 Subject: [PATCH 017/111] Add Byfron logo --- Bloxstrap/Bloxstrap.csproj | 4 +++- Bloxstrap/Bootstrapper.cs | 2 ++ Bloxstrap/Dialogs/BootstrapperDialogForm.cs | 3 +++ Bloxstrap/Dialogs/ByfronDialog.xaml | 3 ++- Bloxstrap/Dialogs/ByfronDialog.xaml.cs | 13 ++++++++++++- Bloxstrap/Dialogs/FluentDialog.xaml.cs | 3 +++ Bloxstrap/Dialogs/IBootstrapperDialog.cs | 1 + Bloxstrap/Properties/Resources.Designer.cs | 2 +- Bloxstrap/Resources/ByfronLogo.png | Bin 0 -> 15113 bytes Bloxstrap/Resources/ByfronLogoDark.png | Bin 0 -> 19543 bytes Bloxstrap/ViewModels/AppearanceViewModel.cs | 2 ++ Bloxstrap/ViewModels/ByfronDialogViewModel.cs | 10 +++++++++- 12 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 Bloxstrap/Resources/ByfronLogo.png create mode 100644 Bloxstrap/Resources/ByfronLogoDark.png diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 60a5348..84c8b59 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -14,7 +14,9 @@ - + + + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 5b915eb..5d57f8c 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -709,6 +709,8 @@ namespace Bloxstrap { Dialog.CancelEnabled = true; Dialog.ProgressStyle = ProgressBarStyle.Continuous; + // Byfron dialog specific + Dialog.VersionVisibility = false; } // compute total bytes to download diff --git a/Bloxstrap/Dialogs/BootstrapperDialogForm.cs b/Bloxstrap/Dialogs/BootstrapperDialogForm.cs index 64e5c5f..3bff244 100644 --- a/Bloxstrap/Dialogs/BootstrapperDialogForm.cs +++ b/Bloxstrap/Dialogs/BootstrapperDialogForm.cs @@ -63,6 +63,9 @@ namespace Bloxstrap.Dialogs _cancelEnabled = value; } } + + // Byfron specific - not required here, bypassing + public bool VersionVisibility { get; set; } #endregion public void ScaleWindow() diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml b/Bloxstrap/Dialogs/ByfronDialog.xaml index 30dd0ab..6bbaa09 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml @@ -15,6 +15,7 @@ + - + diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs index 92b01d6..6ac5cce 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs @@ -2,7 +2,7 @@ using System.Windows; using System.Windows.Forms; using System.Windows.Media; - +using System.Windows.Media.Imaging; using Bloxstrap.Enums; using Bloxstrap.Extensions; using Bloxstrap.ViewModels; @@ -58,6 +58,16 @@ namespace Bloxstrap.Dialogs _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); } } + + public bool VersionVisibility + { + get => _viewModel.VersionNumberVisibility == Visibility.Collapsed; + set + { + _viewModel.VersionNumberVisibility = (value ? Visibility.Visible : Visibility.Collapsed); + _viewModel.OnPropertyChanged(nameof(_viewModel.VersionNumberVisibility)); + } + } #endregion public ByfronDialog() @@ -75,6 +85,7 @@ namespace Bloxstrap.Dialogs _viewModel.Foreground = new SolidColorBrush(Color.FromRgb(57, 59, 61)); _viewModel.IconColor = new SolidColorBrush(Color.FromRgb(57, 59, 61)); _viewModel.ProgressBarBackground = new SolidColorBrush(Color.FromRgb(189, 190, 190)); + _viewModel.ByfronLogo = new BitmapImage(new Uri("pack://application:,,,/Bloxstrap;component/Resources/ByfronLogoDark.png")); } InitializeComponent(); diff --git a/Bloxstrap/Dialogs/FluentDialog.xaml.cs b/Bloxstrap/Dialogs/FluentDialog.xaml.cs index ff65b08..dd88b7c 100644 --- a/Bloxstrap/Dialogs/FluentDialog.xaml.cs +++ b/Bloxstrap/Dialogs/FluentDialog.xaml.cs @@ -63,6 +63,9 @@ namespace Bloxstrap.Dialogs _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); } } + + // Byfron specific - not required here, bypassing + public bool VersionVisibility { get; set; } #endregion public FluentDialog() diff --git a/Bloxstrap/Dialogs/IBootstrapperDialog.cs b/Bloxstrap/Dialogs/IBootstrapperDialog.cs index e66b687..53b816b 100644 --- a/Bloxstrap/Dialogs/IBootstrapperDialog.cs +++ b/Bloxstrap/Dialogs/IBootstrapperDialog.cs @@ -10,6 +10,7 @@ namespace Bloxstrap.Dialogs ProgressBarStyle ProgressStyle { get; set; } int ProgressValue { get; set; } bool CancelEnabled { get; set; } + bool VersionVisibility { get; set; } void ShowBootstrapper(); void CloseBootstrapper(); diff --git a/Bloxstrap/Properties/Resources.Designer.cs b/Bloxstrap/Properties/Resources.Designer.cs index 202cd2e..2e36579 100644 --- a/Bloxstrap/Properties/Resources.Designer.cs +++ b/Bloxstrap/Properties/Resources.Designer.cs @@ -59,7 +59,7 @@ namespace Bloxstrap.Properties { resourceCulture = value; } } - + ///

/// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/Bloxstrap/Resources/ByfronLogo.png b/Bloxstrap/Resources/ByfronLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..5e0c9e8721212f48d6e15c9a0d4971fd098d83a5 GIT binary patch literal 15113 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

3oAyk-M;{J@OknAR^zdstw?QJSZ_42o1ODEKc{ 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�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 E0ZAhxj{pDw literal 0 HcmV?d00001 diff --git a/Bloxstrap/ViewModels/AppearanceViewModel.cs b/Bloxstrap/ViewModels/AppearanceViewModel.cs index ba85815..228385c 100644 --- a/Bloxstrap/ViewModels/AppearanceViewModel.cs +++ b/Bloxstrap/ViewModels/AppearanceViewModel.cs @@ -36,6 +36,8 @@ namespace Bloxstrap.ViewModels dialog.Message = "Style preview - Click Cancel to close"; dialog.CancelEnabled = true; + // Byfron dialog specific + dialog.VersionVisibility = false; dialog.ShowBootstrapper(); } diff --git a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs index 7609046..4b5405c 100644 --- a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs +++ b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs @@ -1,7 +1,10 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Windows; using System.Windows.Media; +using System.Windows.Media.Imaging; using Bloxstrap.Dialogs; +using Bloxstrap.Extensions; namespace Bloxstrap.ViewModels { @@ -15,6 +18,11 @@ namespace Bloxstrap.ViewModels public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(239, 239, 239)); public Brush IconColor { get; set; } = new SolidColorBrush(Color.FromRgb(255, 255, 255)); public Brush ProgressBarBackground { get; set; } = new SolidColorBrush(Color.FromRgb(86, 86, 86)); + public ImageSource ByfronLogo { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Bloxstrap;component/Resources/ByfronLogo.png")); + + // When cancel button is visible, version number is hidden. + public Visibility VersionNumberVisibility { get; set; } = Visibility.Collapsed; + public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) { } From 2a24a0526fe470d719dbc9b1303dffbf46eb1059 Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Sat, 13 May 2023 12:05:20 +0800 Subject: [PATCH 018/111] Fix padding and visibility --- Bloxstrap/Dialogs/ByfronDialog.xaml | 2 +- Bloxstrap/Dialogs/ByfronDialog.xaml.cs | 2 +- Bloxstrap/ViewModels/ByfronDialogViewModel.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml b/Bloxstrap/Dialogs/ByfronDialog.xaml index 6bbaa09..1e779ed 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml @@ -23,7 +23,7 @@ - + diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs index 6ac5cce..411930c 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs @@ -61,7 +61,7 @@ namespace Bloxstrap.Dialogs public bool VersionVisibility { - get => _viewModel.VersionNumberVisibility == Visibility.Collapsed; + get => _viewModel.VersionNumberVisibility == Visibility.Visible; set { _viewModel.VersionNumberVisibility = (value ? Visibility.Visible : Visibility.Collapsed); diff --git a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs index 4b5405c..bbaefdb 100644 --- a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs +++ b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs @@ -21,7 +21,7 @@ namespace Bloxstrap.ViewModels public ImageSource ByfronLogo { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Bloxstrap;component/Resources/ByfronLogo.png")); // When cancel button is visible, version number is hidden. - public Visibility VersionNumberVisibility { get; set; } = Visibility.Collapsed; + public Visibility VersionNumberVisibility { get; set; } = Visibility.Visible; public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) { From 58932f4b340a81f4b12a9fcb9349e217add307da Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Sat, 13 May 2023 12:13:15 +0800 Subject: [PATCH 019/111] =?UTF-8?q?i=20forgor=20=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bloxstrap/Bootstrapper.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 5d57f8c..44d4e0d 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -793,7 +793,11 @@ namespace Bloxstrap } if (Dialog is not null) + { Dialog.CancelEnabled = false; + // Byfron dialog specific + Dialog.VersionVisibility = true; + } App.State.Prop.VersionGuid = _latestVersionGuid; From 969dbbbf922e7bb2b86c6553b663e23410a5622c Mon Sep 17 00:00:00 2001 From: 1011025m <37438176+1011025m@users.noreply.github.com> Date: Sat, 13 May 2023 20:43:13 +0800 Subject: [PATCH 020/111] Regression, it didn't have to be over engineered --- Bloxstrap/Bootstrapper.cs | 6 ------ Bloxstrap/Dialogs/BootstrapperDialogForm.cs | 3 --- Bloxstrap/Dialogs/ByfronDialog.xaml.cs | 10 +--------- Bloxstrap/Dialogs/FluentDialog.xaml.cs | 3 --- Bloxstrap/Dialogs/IBootstrapperDialog.cs | 1 - Bloxstrap/ViewModels/AppearanceViewModel.cs | 2 -- 6 files changed, 1 insertion(+), 24 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 44d4e0d..5b915eb 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -709,8 +709,6 @@ namespace Bloxstrap { Dialog.CancelEnabled = true; Dialog.ProgressStyle = ProgressBarStyle.Continuous; - // Byfron dialog specific - Dialog.VersionVisibility = false; } // compute total bytes to download @@ -793,11 +791,7 @@ namespace Bloxstrap } if (Dialog is not null) - { Dialog.CancelEnabled = false; - // Byfron dialog specific - Dialog.VersionVisibility = true; - } App.State.Prop.VersionGuid = _latestVersionGuid; diff --git a/Bloxstrap/Dialogs/BootstrapperDialogForm.cs b/Bloxstrap/Dialogs/BootstrapperDialogForm.cs index 3bff244..64e5c5f 100644 --- a/Bloxstrap/Dialogs/BootstrapperDialogForm.cs +++ b/Bloxstrap/Dialogs/BootstrapperDialogForm.cs @@ -63,9 +63,6 @@ namespace Bloxstrap.Dialogs _cancelEnabled = value; } } - - // Byfron specific - not required here, bypassing - public bool VersionVisibility { get; set; } #endregion public void ScaleWindow() diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs index 411930c..3a0a1f7 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs @@ -55,16 +55,8 @@ namespace Bloxstrap.Dialogs set { _viewModel.CancelButtonVisibility = (value ? Visibility.Visible : Visibility.Collapsed); + _viewModel.VersionNumberVisibility = (value ? Visibility.Collapsed : Visibility.Visible); _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); - } - } - - public bool VersionVisibility - { - get => _viewModel.VersionNumberVisibility == Visibility.Visible; - set - { - _viewModel.VersionNumberVisibility = (value ? Visibility.Visible : Visibility.Collapsed); _viewModel.OnPropertyChanged(nameof(_viewModel.VersionNumberVisibility)); } } diff --git a/Bloxstrap/Dialogs/FluentDialog.xaml.cs b/Bloxstrap/Dialogs/FluentDialog.xaml.cs index dd88b7c..ff65b08 100644 --- a/Bloxstrap/Dialogs/FluentDialog.xaml.cs +++ b/Bloxstrap/Dialogs/FluentDialog.xaml.cs @@ -63,9 +63,6 @@ namespace Bloxstrap.Dialogs _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); } } - - // Byfron specific - not required here, bypassing - public bool VersionVisibility { get; set; } #endregion public FluentDialog() diff --git a/Bloxstrap/Dialogs/IBootstrapperDialog.cs b/Bloxstrap/Dialogs/IBootstrapperDialog.cs index 53b816b..e66b687 100644 --- a/Bloxstrap/Dialogs/IBootstrapperDialog.cs +++ b/Bloxstrap/Dialogs/IBootstrapperDialog.cs @@ -10,7 +10,6 @@ namespace Bloxstrap.Dialogs ProgressBarStyle ProgressStyle { get; set; } int ProgressValue { get; set; } bool CancelEnabled { get; set; } - bool VersionVisibility { get; set; } void ShowBootstrapper(); void CloseBootstrapper(); diff --git a/Bloxstrap/ViewModels/AppearanceViewModel.cs b/Bloxstrap/ViewModels/AppearanceViewModel.cs index 228385c..ba85815 100644 --- a/Bloxstrap/ViewModels/AppearanceViewModel.cs +++ b/Bloxstrap/ViewModels/AppearanceViewModel.cs @@ -36,8 +36,6 @@ namespace Bloxstrap.ViewModels dialog.Message = "Style preview - Click Cancel to close"; dialog.CancelEnabled = true; - // Byfron dialog specific - dialog.VersionVisibility = false; dialog.ShowBootstrapper(); } From 9da925cd5489e0ed25ad3a17a991492af0e83b97 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 13 May 2023 14:23:32 +0100 Subject: [PATCH 021/111] Add fake Byfron logo Also undo some changes for version number and stuff, I'll do that later in a sec --- Bloxstrap/App.xaml | 2 +- Bloxstrap/Bloxstrap.csproj | 6 ++-- Bloxstrap/Dialogs/ByfronDialog.xaml | 27 +++++++----------- Bloxstrap/Dialogs/ByfronDialog.xaml.cs | 5 ++-- .../ByfronDialog/ByfronLogoDark.jpg | Bin 0 -> 6262 bytes .../ByfronDialog/ByfronLogoLight.jpg | Bin 0 -> 5718 bytes Bloxstrap/Resources/ByfronLogo.png | Bin 15113 -> 0 bytes Bloxstrap/Resources/ByfronLogoDark.png | Bin 19543 -> 0 bytes .../Fonts/Rubik-VariableFont_wght.ttf | Bin Bloxstrap/ViewModels/ByfronDialogViewModel.cs | 7 ++--- 10 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg create mode 100644 Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoLight.jpg delete mode 100644 Bloxstrap/Resources/ByfronLogo.png delete mode 100644 Bloxstrap/Resources/ByfronLogoDark.png rename Bloxstrap/{ => Resources}/Fonts/Rubik-VariableFont_wght.ttf (100%) diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml index 3ac28c3..c46ad48 100644 --- a/Bloxstrap/App.xaml +++ b/Bloxstrap/App.xaml @@ -12,7 +12,7 @@ - pack://application:,,,/Fonts/#Rubik Light + pack://application:,,,/Resources/Fonts/#Rubik Light diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 84c8b59..544d5cb 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml b/Bloxstrap/Dialogs/ByfronDialog.xaml index 1e779ed..2747e05 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml @@ -12,21 +12,17 @@ WindowStartupLocation="CenterScreen" AllowsTransparency="True" Background="Transparent"> - - - - - - - - - + + + + + @@ -44,6 +40,5 @@ - diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs index 3a0a1f7..f9aa671 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs @@ -3,6 +3,7 @@ using System.Windows; using System.Windows.Forms; using System.Windows.Media; using System.Windows.Media.Imaging; + using Bloxstrap.Enums; using Bloxstrap.Extensions; using Bloxstrap.ViewModels; @@ -55,9 +56,7 @@ namespace Bloxstrap.Dialogs set { _viewModel.CancelButtonVisibility = (value ? Visibility.Visible : Visibility.Collapsed); - _viewModel.VersionNumberVisibility = (value ? Visibility.Collapsed : Visibility.Visible); _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); - _viewModel.OnPropertyChanged(nameof(_viewModel.VersionNumberVisibility)); } } #endregion @@ -77,7 +76,7 @@ namespace Bloxstrap.Dialogs _viewModel.Foreground = new SolidColorBrush(Color.FromRgb(57, 59, 61)); _viewModel.IconColor = new SolidColorBrush(Color.FromRgb(57, 59, 61)); _viewModel.ProgressBarBackground = new SolidColorBrush(Color.FromRgb(189, 190, 190)); - _viewModel.ByfronLogo = new BitmapImage(new Uri("pack://application:,,,/Bloxstrap;component/Resources/ByfronLogoDark.png")); + _viewModel.ByfronLogoLocation = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoLight.jpg")); } InitializeComponent(); diff --git a/Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg b/Bloxstrap/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg new file mode 100644 index 0000000000000000000000000000000000000000..770e46b1c313f8d70de539f03b731302f6ce7588 GIT binary patch literal 6262 zcmbVQdpwi>+rL@PG-n}}Qz@qsA+pLBNm40t+}EMRLQ1TfjpP(1DoSNJr4nn7#j@|r z94a~H7&|ysl1(*tHrsRS_q?9p>-D^zKc46L>~&rHZ1?AL-`D%T-g{ryJ<*7W2`C

*E;i3^{*QMpj*8gQk|z<}Jos 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

3oAyk-M;{J@OknAR^zdstw?QJSZ_42o1ODEKc{ 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�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 E0ZAhxj{pDw diff --git a/Bloxstrap/Fonts/Rubik-VariableFont_wght.ttf b/Bloxstrap/Resources/Fonts/Rubik-VariableFont_wght.ttf similarity index 100% rename from Bloxstrap/Fonts/Rubik-VariableFont_wght.ttf rename to Bloxstrap/Resources/Fonts/Rubik-VariableFont_wght.ttf diff --git a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs index bbaefdb..30cae23 100644 --- a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs +++ b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs @@ -3,8 +3,8 @@ using System.ComponentModel; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; + using Bloxstrap.Dialogs; -using Bloxstrap.Extensions; namespace Bloxstrap.ViewModels { @@ -13,15 +13,12 @@ namespace Bloxstrap.ViewModels public string Version => $"Bloxstrap v{App.Version}"; // Using dark theme for default values. + public ImageSource ByfronLogoLocation { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg")); public Thickness DialogBorder { get; set; } = new Thickness(0); public Brush Background { get; set; } = Brushes.Black; public Brush Foreground { get; set; } = new SolidColorBrush(Color.FromRgb(239, 239, 239)); public Brush IconColor { get; set; } = new SolidColorBrush(Color.FromRgb(255, 255, 255)); public Brush ProgressBarBackground { get; set; } = new SolidColorBrush(Color.FromRgb(86, 86, 86)); - public ImageSource ByfronLogo { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Bloxstrap;component/Resources/ByfronLogo.png")); - - // When cancel button is visible, version number is hidden. - public Visibility VersionNumberVisibility { get; set; } = Visibility.Visible; public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) { From 1f91c309ca94828998bb692beb5b909e825932d2 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 14 May 2023 01:39:28 +0100 Subject: [PATCH 022/111] Add client version number Finalizing the fake Byfron style --- Bloxstrap/Bootstrapper.cs | 9 +++++--- Bloxstrap/Dialogs/ByfronDialog.xaml | 7 +++--- Bloxstrap/Dialogs/ByfronDialog.xaml.cs | 3 +++ Bloxstrap/ViewModels/ByfronDialogViewModel.cs | 23 +++++++++++++++++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 5b915eb..8df2c36 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -17,7 +17,6 @@ using Bloxstrap.Enums; using Bloxstrap.Integrations; using Bloxstrap.Models; using Bloxstrap.Tools; -using System.Globalization; namespace Bloxstrap { @@ -111,6 +110,10 @@ namespace Bloxstrap { App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); + // yea idk + if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) + message = message.Replace("...", ""); + if (Dialog is not null) Dialog.Message = message; } @@ -790,11 +793,11 @@ namespace Bloxstrap } } + App.State.Prop.VersionGuid = _latestVersionGuid; + if (Dialog is not null) Dialog.CancelEnabled = false; - App.State.Prop.VersionGuid = _latestVersionGuid; - _isInstalling = false; } diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml b/Bloxstrap/Dialogs/ByfronDialog.xaml index 2747e05..d2cda20 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml @@ -15,24 +15,25 @@ - + - + - + diff --git a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs index f9aa671..a689121 100644 --- a/Bloxstrap/Dialogs/ByfronDialog.xaml.cs +++ b/Bloxstrap/Dialogs/ByfronDialog.xaml.cs @@ -57,6 +57,9 @@ namespace Bloxstrap.Dialogs { _viewModel.CancelButtonVisibility = (value ? Visibility.Visible : Visibility.Collapsed); _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); + + _viewModel.OnPropertyChanged(nameof(_viewModel.VersionTextVisibility)); + _viewModel.OnPropertyChanged(nameof(_viewModel.VersionText)); } } #endregion diff --git a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs index 30cae23..6178606 100644 --- a/Bloxstrap/ViewModels/ByfronDialogViewModel.cs +++ b/Bloxstrap/ViewModels/ByfronDialogViewModel.cs @@ -1,5 +1,7 @@ using System; using System.ComponentModel; +using System.Diagnostics; +using System.IO; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -10,8 +12,6 @@ namespace Bloxstrap.ViewModels { public class ByfronDialogViewModel : FluentDialogViewModel, INotifyPropertyChanged { - public string Version => $"Bloxstrap v{App.Version}"; - // Using dark theme for default values. public ImageSource ByfronLogoLocation { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg")); public Thickness DialogBorder { get; set; } = new Thickness(0); @@ -20,6 +20,25 @@ namespace Bloxstrap.ViewModels public Brush IconColor { get; set; } = new SolidColorBrush(Color.FromRgb(255, 255, 255)); public Brush ProgressBarBackground { get; set; } = new SolidColorBrush(Color.FromRgb(86, 86, 86)); + public Visibility VersionTextVisibility => CancelButtonVisibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed; + public string VersionText + { + get + { + string playerLocation = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid, "RobloxPlayerBeta.exe"); + + if (!File.Exists(playerLocation)) + return ""; + + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(playerLocation); + + if (versionInfo.ProductVersion is null) + return ""; + + return versionInfo.ProductVersion.Replace(", ", "."); + } + } + public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) { } From a50a032e84fb8f692f9685dde15b9e6516a8ffc0 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 14 May 2023 01:51:28 +0100 Subject: [PATCH 023/111] credits :D --- Bloxstrap/Views/Pages/AboutPage.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/Views/Pages/AboutPage.xaml b/Bloxstrap/Views/Pages/AboutPage.xaml index a455e90..9c984eb 100644 --- a/Bloxstrap/Views/Pages/AboutPage.xaml +++ b/Bloxstrap/Views/Pages/AboutPage.xaml @@ -70,7 +70,7 @@ 1011025m - + sitiom From 6d72ec973ee9c867f86a3dccf01ccab3dcae79ca Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 14 May 2023 02:05:38 +0100 Subject: [PATCH 024/111] Move auto channel change preference moved to installation tab to make it clearer that it's related to channel preference --- Bloxstrap/ViewModels/BehaviourViewModel.cs | 21 +------------------ Bloxstrap/ViewModels/InstallationViewModel.cs | 15 +++++++++++++ Bloxstrap/Views/Pages/BehaviourPage.xaml | 9 -------- Bloxstrap/Views/Pages/InstallationPage.xaml | 9 ++++++++ 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/Bloxstrap/ViewModels/BehaviourViewModel.cs b/Bloxstrap/ViewModels/BehaviourViewModel.cs index be4812e..86bb853 100644 --- a/Bloxstrap/ViewModels/BehaviourViewModel.cs +++ b/Bloxstrap/ViewModels/BehaviourViewModel.cs @@ -1,9 +1,4 @@ -using System.Collections.Generic; -using System.Linq; - -using Bloxstrap.Enums; - -namespace Bloxstrap.ViewModels +namespace Bloxstrap.ViewModels { public class BehaviourViewModel { @@ -24,19 +19,5 @@ namespace Bloxstrap.ViewModels get => App.Settings.Prop.MultiInstanceLaunching; set => App.Settings.Prop.MultiInstanceLaunching = value; } - - // todo - move to enum attributes? - public IReadOnlyDictionary ChannelChangeModes => new Dictionary - { - { "Change automatically", ChannelChangeMode.Automatic }, - { "Always prompt", ChannelChangeMode.Prompt }, - { "Never change", ChannelChangeMode.Ignore }, - }; - - public string SelectedChannelChangeMode - { - get => ChannelChangeModes.FirstOrDefault(x => x.Value == App.Settings.Prop.ChannelChangeMode).Key; - set => App.Settings.Prop.ChannelChangeMode = ChannelChangeModes[value]; - } } } diff --git a/Bloxstrap/ViewModels/InstallationViewModel.cs b/Bloxstrap/ViewModels/InstallationViewModel.cs index 7eca88e..d64607b 100644 --- a/Bloxstrap/ViewModels/InstallationViewModel.cs +++ b/Bloxstrap/ViewModels/InstallationViewModel.cs @@ -10,6 +10,7 @@ using System.Windows.Input; using CommunityToolkit.Mvvm.Input; +using Bloxstrap.Enums; using Bloxstrap.Models; namespace Bloxstrap.ViewModels @@ -117,5 +118,19 @@ namespace Bloxstrap.ViewModels // cant use data bindings so i have to do whatever tf this is public Visibility ChannelComboBoxVisibility => ManualChannelEntry ? Visibility.Collapsed : Visibility.Visible; public Visibility ChannelTextBoxVisibility => ManualChannelEntry ? Visibility.Visible : Visibility.Collapsed; + + // todo - move to enum attributes? + public IReadOnlyDictionary ChannelChangeModes => new Dictionary + { + { "Change automatically", ChannelChangeMode.Automatic }, + { "Always prompt", ChannelChangeMode.Prompt }, + { "Never change", ChannelChangeMode.Ignore }, + }; + + public string SelectedChannelChangeMode + { + get => ChannelChangeModes.FirstOrDefault(x => x.Value == App.Settings.Prop.ChannelChangeMode).Key; + set => App.Settings.Prop.ChannelChangeMode = ChannelChangeModes[value]; + } } } diff --git a/Bloxstrap/Views/Pages/BehaviourPage.xaml b/Bloxstrap/Views/Pages/BehaviourPage.xaml index 0ed442b..daaa9fe 100644 --- a/Bloxstrap/Views/Pages/BehaviourPage.xaml +++ b/Bloxstrap/Views/Pages/BehaviourPage.xaml @@ -40,14 +40,5 @@ - - - - - - - - - diff --git a/Bloxstrap/Views/Pages/InstallationPage.xaml b/Bloxstrap/Views/Pages/InstallationPage.xaml index ca4dc89..007f021 100644 --- a/Bloxstrap/Views/Pages/InstallationPage.xaml +++ b/Bloxstrap/Views/Pages/InstallationPage.xaml @@ -122,5 +122,14 @@ + + + + + + + + + From 64978aecb0877cee5282f82681dc864c0f9bfb93 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 14 May 2023 02:21:21 +0100 Subject: [PATCH 025/111] Add FFlag preset for enabling old textures (#215) --- Bloxstrap/Singletons/FastFlagManager.cs | 3 +++ Bloxstrap/ViewModels/FastFlagsViewModel.cs | 6 ++++++ Bloxstrap/Views/Pages/FastFlagsPage.xaml | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index 4a5b26f..e52e52b 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -15,6 +15,9 @@ namespace Bloxstrap.Singletons // to delete a fastflag, set the value to null public Dictionary Changes = new(); + // this is the value of the 'FStringPartTexturePackTablePre2022' flag + public const string OldTexturesFlagValue = "{\"foil\":{\"ids\":[\"rbxassetid://7546645012\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"brick\":{\"ids\":[\"rbxassetid://7546650097\",\"rbxassetid://7546645118\"],\"color\":[204,201,200,232]},\"cobblestone\":{\"ids\":[\"rbxassetid://7546652947\",\"rbxassetid://7546645118\"],\"color\":[212,200,187,250]},\"concrete\":{\"ids\":[\"rbxassetid://7546653951\",\"rbxassetid://7546654144\"],\"color\":[208,208,208,255]},\"diamondplate\":{\"ids\":[\"rbxassetid://7547162198\",\"rbxassetid://7546645118\"],\"color\":[170,170,170,255]},\"fabric\":{\"ids\":[\"rbxassetid://7547101130\",\"rbxassetid://7546645118\"],\"color\":[105,104,102,244]},\"glass\":{\"ids\":[\"rbxassetid://7547304948\",\"rbxassetid://7546645118\"],\"color\":[254,254,254,7]},\"granite\":{\"ids\":[\"rbxassetid://7547164710\",\"rbxassetid://7546645118\"],\"color\":[113,113,113,255]},\"grass\":{\"ids\":[\"rbxassetid://7547169285\",\"rbxassetid://7546645118\"],\"color\":[165,165,159,255]},\"ice\":{\"ids\":[\"rbxassetid://7547171356\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"marble\":{\"ids\":[\"rbxassetid://7547177270\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"metal\":{\"ids\":[\"rbxassetid://7547288171\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"pebble\":{\"ids\":[\"rbxassetid://7547291361\",\"rbxassetid://7546645118\"],\"color\":[208,208,208,255]},\"corrodedmetal\":{\"ids\":[\"rbxassetid://7547184629\",\"rbxassetid://7546645118\"],\"color\":[159,119,95,200]},\"sand\":{\"ids\":[\"rbxassetid://7547295153\",\"rbxassetid://7546645118\"],\"color\":[220,220,220,255]},\"slate\":{\"ids\":[\"rbxassetid://7547298114\",\"rbxassetid://7547298323\"],\"color\":[193,193,193,255]},\"wood\":{\"ids\":[\"rbxassetid://7547303225\",\"rbxassetid://7547298786\"],\"color\":[227,227,227,255]},\"woodplanks\":{\"ids\":[\"rbxassetid://7547332968\",\"rbxassetid://7546645118\"],\"color\":[212,209,203,255]},\"asphalt\":{\"ids\":[\"rbxassetid://9873267379\",\"rbxassetid://9438410548\"],\"color\":[123,123,123,234]},\"basalt\":{\"ids\":[\"rbxassetid://9873270487\",\"rbxassetid://9438413638\"],\"color\":[154,154,153,238]},\"crackedlava\":{\"ids\":[\"rbxassetid://9438582231\",\"rbxassetid://9438453972\"],\"color\":[74,78,80,156]},\"glacier\":{\"ids\":[\"rbxassetid://9438851661\",\"rbxassetid://9438453972\"],\"color\":[226,229,229,243]},\"ground\":{\"ids\":[\"rbxassetid://9439044431\",\"rbxassetid://9438453972\"],\"color\":[114,114,112,240]},\"leafygrass\":{\"ids\":[\"rbxassetid://9873288083\",\"rbxassetid://9438453972\"],\"color\":[121,117,113,234]},\"limestone\":{\"ids\":[\"rbxassetid://9873289812\",\"rbxassetid://9438453972\"],\"color\":[235,234,230,250]},\"mud\":{\"ids\":[\"rbxassetid://9873319819\",\"rbxassetid://9438453972\"],\"color\":[130,130,130,252]},\"pavement\":{\"ids\":[\"rbxassetid://9873322398\",\"rbxassetid://9438453972\"],\"color\":[142,142,144,236]},\"rock\":{\"ids\":[\"rbxassetid://9873515198\",\"rbxassetid://9438453972\"],\"color\":[154,154,154,248]},\"salt\":{\"ids\":[\"rbxassetid://9439566986\",\"rbxassetid://9438453972\"],\"color\":[220,220,221,255]},\"sandstone\":{\"ids\":[\"rbxassetid://9873521380\",\"rbxassetid://9438453972\"],\"color\":[174,171,169,246]},\"snow\":{\"ids\":[\"rbxassetid://9439632387\",\"rbxassetid://9438453972\"],\"color\":[218,218,218,255]}}"; + // only one missing here is Metal because lol public static IReadOnlyDictionary RenderingModes => new Dictionary { diff --git a/Bloxstrap/ViewModels/FastFlagsViewModel.cs b/Bloxstrap/ViewModels/FastFlagsViewModel.cs index 6531153..5993545 100644 --- a/Bloxstrap/ViewModels/FastFlagsViewModel.cs +++ b/Bloxstrap/ViewModels/FastFlagsViewModel.cs @@ -102,6 +102,12 @@ namespace Bloxstrap.ViewModels set => App.FastFlags.SetValue("FFlagFixGraphicsQuality", value ? "True" : null); } + public bool Pre2022TexturesEnabled + { + get => App.FastFlags.GetValue("FStringPartTexturePackTable2022") == FastFlagManager.OldTexturesFlagValue; + set => App.FastFlags.SetValue("FStringPartTexturePackTable2022", value ? FastFlagManager.OldTexturesFlagValue : null); + } + public bool MobileLuaAppInterfaceEnabled { get => App.FastFlags.GetValue("FFlagLuaAppSystemBar") == "False"; diff --git a/Bloxstrap/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/Views/Pages/FastFlagsPage.xaml index 1d9ff6a..4a66575 100644 --- a/Bloxstrap/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/Views/Pages/FastFlagsPage.xaml @@ -56,7 +56,7 @@ - + @@ -82,7 +82,16 @@ - + + + + + + + + + + @@ -92,7 +101,7 @@ - + From cb551ad0d651d9eec0b931ab89be94b318fda003 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 14 May 2023 02:48:33 +0100 Subject: [PATCH 026/111] Add FFlag preset for GUI hiding many thanks to https://github.com/pizzaboxer/bloxstrap/issues/230#issuecomment-1542966228 --- Bloxstrap/ViewModels/FastFlagsViewModel.cs | 42 ++++++++++++---------- Bloxstrap/Views/Pages/FastFlagsPage.xaml | 18 ++++++++-- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Bloxstrap/ViewModels/FastFlagsViewModel.cs b/Bloxstrap/ViewModels/FastFlagsViewModel.cs index 5993545..a7b0b7f 100644 --- a/Bloxstrap/ViewModels/FastFlagsViewModel.cs +++ b/Bloxstrap/ViewModels/FastFlagsViewModel.cs @@ -63,6 +63,18 @@ namespace Bloxstrap.ViewModels } } + public bool AlternateGraphicsSelectorEnabled + { + get => App.FastFlags.GetValue("FFlagFixGraphicsQuality") == "True"; + set => App.FastFlags.SetValue("FFlagFixGraphicsQuality", value ? "True" : null); + } + + public bool Pre2022TexturesEnabled + { + get => App.FastFlags.GetValue("FStringPartTexturePackTable2022") == FastFlagManager.OldTexturesFlagValue; + set => App.FastFlags.SetValue("FStringPartTexturePackTable2022", value ? FastFlagManager.OldTexturesFlagValue : null); + } + public IReadOnlyDictionary> IGMenuVersions => FastFlagManager.IGMenuVersions; public string SelectedIGMenuVersion @@ -96,24 +108,6 @@ namespace Bloxstrap.ViewModels } } - public bool AlternateGraphicsSelectorEnabled - { - get => App.FastFlags.GetValue("FFlagFixGraphicsQuality") == "True"; - set => App.FastFlags.SetValue("FFlagFixGraphicsQuality", value ? "True" : null); - } - - public bool Pre2022TexturesEnabled - { - get => App.FastFlags.GetValue("FStringPartTexturePackTable2022") == FastFlagManager.OldTexturesFlagValue; - set => App.FastFlags.SetValue("FStringPartTexturePackTable2022", value ? FastFlagManager.OldTexturesFlagValue : null); - } - - public bool MobileLuaAppInterfaceEnabled - { - get => App.FastFlags.GetValue("FFlagLuaAppSystemBar") == "False"; - set => App.FastFlags.SetValue("FFlagLuaAppSystemBar", value ? "False" : null); - } - public IReadOnlyDictionary LightingTechnologies => FastFlagManager.LightingTechnologies; // this is basically the same as the code for rendering selection, maybe this could be abstracted in some way? @@ -142,5 +136,17 @@ namespace Bloxstrap.ViewModels App.FastFlags.SetValue(LightingTechnologies[value], "True"); } } + + public bool GuiHidingEnabled + { + get => App.FastFlags.GetValue("DFIntCanHideGuiGroupId") == "32380007"; + set => App.FastFlags.SetValue("DFIntCanHideGuiGroupId", value ? "32380007" : null); + } + + public bool MobileLuaAppInterfaceEnabled + { + get => App.FastFlags.GetValue("FFlagLuaAppSystemBar") == "False"; + set => App.FastFlags.SetValue("FFlagLuaAppSystemBar", value ? "False" : null); + } } } diff --git a/Bloxstrap/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/Views/Pages/FastFlagsPage.xaml index 4a66575..4d0ae46 100644 --- a/Bloxstrap/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/Views/Pages/FastFlagsPage.xaml @@ -13,7 +13,8 @@ - + + @@ -91,8 +92,8 @@ - - + + @@ -106,6 +107,17 @@ + + + + + + TODO - add text for wiki page for controls when done. You must be in the Bloxstrap group for it to work. + + + + + From 22e1623d2380bd935daf1666044d4d9f6f75e77c Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 14 May 2023 02:53:22 +0100 Subject: [PATCH 027/111] Fix bug with ClientAppSettings edit button state --- Bloxstrap/Views/Pages/FastFlagsPage.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/Views/Pages/FastFlagsPage.xaml index 4d0ae46..930d83d 100644 --- a/Bloxstrap/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/Views/Pages/FastFlagsPage.xaml @@ -21,7 +21,7 @@ - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TODO - add text for wiki page for controls when done. You must be in the Bloxstrap group for it to work. - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TODO - add text for wiki page for controls when done. You must be in the Bloxstrap group for it to work. + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml index 464f46d..81a9bac 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml @@ -1,135 +1,134 @@  + + - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml index 5f9021b..234dfd0 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml @@ -1,106 +1,106 @@  - - + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml index 1f1b805..8d0e4f1 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml @@ -1,128 +1,127 @@ - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A Windows feature that intends to improve fullscreen performance. See here for more information. - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A Windows feature that intends to improve fullscreen performance. See here for more information. + + + + + + From 50bb408e188ad1881fa8fa3d3b96664c33d45acb Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 16 May 2023 10:51:39 +0100 Subject: [PATCH 036/111] Update ci.yml --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1921310..4d3ab1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: CI -on: [push, pull_request] - +on: + push: + paths-ignore: + - 'Resources/**' + pull_request: + paths-ignore: + - 'Resources/**' + jobs: build: strategy: From 00c9bd2e55302660683803d09cec42ac35ace746 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 16 May 2023 11:03:19 +0100 Subject: [PATCH 037/111] Make Resources folder for emoji font mod added as a submodule to ensure it's fixed on a specific commit tag for added security --- .gitmodules | 3 +++ Resources/Mods/rbxcustom-fontemojis | 1 + 2 files changed, 4 insertions(+) create mode 160000 Resources/Mods/rbxcustom-fontemojis diff --git a/.gitmodules b/.gitmodules index 1b06fae..0560126 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "wpfui"] path = wpfui url = https://github.com/pizzaboxer/wpfui.git +[submodule "Resources/Mods/rbxcustom-fontemojis"] + path = Resources/Mods/rbxcustom-fontemojis + url = https://github.com/NikSavchenk0/rbxcustom-fontemojis diff --git a/Resources/Mods/rbxcustom-fontemojis b/Resources/Mods/rbxcustom-fontemojis new file mode 160000 index 0000000..8a552f4 --- /dev/null +++ b/Resources/Mods/rbxcustom-fontemojis @@ -0,0 +1 @@ +Subproject commit 8a552f4aaaecfa58d6bd9b0540e1ac16e81faadb From 95fcdf4edaf1c6590d4bb5fd80a8d23c95ea643a Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 16 May 2023 11:05:29 +0100 Subject: [PATCH 038/111] Revert "Make Resources folder for emoji font mod" This reverts commit 00c9bd2e55302660683803d09cec42ac35ace746. --- .gitmodules | 3 --- Resources/Mods/rbxcustom-fontemojis | 1 - 2 files changed, 4 deletions(-) delete mode 160000 Resources/Mods/rbxcustom-fontemojis diff --git a/.gitmodules b/.gitmodules index 0560126..1b06fae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "wpfui"] path = wpfui url = https://github.com/pizzaboxer/wpfui.git -[submodule "Resources/Mods/rbxcustom-fontemojis"] - path = Resources/Mods/rbxcustom-fontemojis - url = https://github.com/NikSavchenk0/rbxcustom-fontemojis diff --git a/Resources/Mods/rbxcustom-fontemojis b/Resources/Mods/rbxcustom-fontemojis deleted file mode 160000 index 8a552f4..0000000 --- a/Resources/Mods/rbxcustom-fontemojis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8a552f4aaaecfa58d6bd9b0540e1ac16e81faadb From 5d94ca7944c6d7ffeb7bbc7af16f273cd05ec325 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 16 May 2023 11:05:33 +0100 Subject: [PATCH 039/111] Revert "Update ci.yml" This reverts commit 50bb408e188ad1881fa8fa3d3b96664c33d45acb. --- .github/workflows/ci.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d3ab1f..1921310 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,6 @@ name: CI -on: - push: - paths-ignore: - - 'Resources/**' - pull_request: - paths-ignore: - - 'Resources/**' - +on: [push, pull_request] + jobs: build: strategy: From a740f99b509242bba4d894f0345ca773b868f9ae Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 18 May 2023 11:24:09 +0100 Subject: [PATCH 040/111] Add support for emoij selection (#148) --- Bloxstrap/Bootstrapper.cs | 25 ++++++++++++++ Bloxstrap/Enums/EmojiType.cs | 11 ++++++ Bloxstrap/Extensions/EmojiTypeEx.cs | 34 +++++++++++++++++++ Bloxstrap/Models/Settings.cs | 1 + Bloxstrap/Models/State.cs | 3 ++ Bloxstrap/UI/Menu/ViewModels/ModsViewModel.cs | 14 +++++++- Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml | 11 ++++++ 7 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 Bloxstrap/Enums/EmojiType.cs create mode 100644 Bloxstrap/Extensions/EmojiTypeEx.cs diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 75f27ae..851642c 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -13,6 +13,7 @@ using System.Windows; using Microsoft.Win32; using Bloxstrap.Enums; +using Bloxstrap.Extensions; using Bloxstrap.Integrations; using Bloxstrap.Models; using Bloxstrap.Tools; @@ -915,6 +916,30 @@ namespace Bloxstrap await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); + // emoji presets are downloaded remotely from github due to how large they are + string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); + + if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + App.State.Prop.CurrentEmojiType = EmojiType.Default; + } + else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); + + var response = await App.HttpClient.GetAsync(remoteEmojiLocation); + await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + + App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; + } + foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) { // get relative directory path diff --git a/Bloxstrap/Enums/EmojiType.cs b/Bloxstrap/Enums/EmojiType.cs new file mode 100644 index 0000000..cdefefd --- /dev/null +++ b/Bloxstrap/Enums/EmojiType.cs @@ -0,0 +1,11 @@ +namespace Bloxstrap.Enums +{ + public enum EmojiType + { + Default, + Catmoji, + Windows11, + Windows10, + Windows8 + } +} diff --git a/Bloxstrap/Extensions/EmojiTypeEx.cs b/Bloxstrap/Extensions/EmojiTypeEx.cs new file mode 100644 index 0000000..5978b66 --- /dev/null +++ b/Bloxstrap/Extensions/EmojiTypeEx.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +using Bloxstrap.Enums; + +namespace Bloxstrap.Extensions +{ + static class EmojiTypeEx + { + public static IReadOnlyDictionary Selections => new Dictionary + { + { "Default (Twemoji)", EmojiType.Default }, + { "Catmoji", EmojiType.Catmoji }, + { "Windows 11", EmojiType.Windows11 }, + { "Windows 10", EmojiType.Windows10 }, + { "Windows 8", EmojiType.Windows8 }, + }; + + public static IReadOnlyDictionary Filenames => new Dictionary + { + { EmojiType.Catmoji, "Catmoji.ttf" }, + { EmojiType.Windows11, "Win1122H2SegoeUIEmoji.ttf" }, + { EmojiType.Windows10, "Win10April2018SegoeUIEmoji.ttf" }, + { EmojiType.Windows8, "Win8.1SegoeUIEmoji.ttf" }, + }; + + public static string GetRemoteLocation(this EmojiType emojiType) + { + if (emojiType == EmojiType.Default) + return ""; + + return $"https://github.com/NikSavchenk0/rbxcustom-fontemojis/raw/8a552f4aaaecfa58d6bd9b0540e1ac16e81faadb/{Filenames[emojiType]}"; + } + } +} diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index cfb173f..477d324 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -31,6 +31,7 @@ namespace Bloxstrap.Models public bool UseOldCharacterSounds { get; set; } = false; public bool UseOldMouseCursor { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false; + public EmojiType PreferredEmojiType { get; set; } = EmojiType.Default; public bool DisableFullscreenOptimizations { get; set; } = false; } } diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index 5b59a9b..cd1a7fc 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -1,10 +1,13 @@ using System.Collections.Generic; +using Bloxstrap.Enums; + namespace Bloxstrap.Models { public class State { public string VersionGuid { get; set; } = ""; + public EmojiType CurrentEmojiType { get; set; } = EmojiType.Default; public List ModManifest { get; set; } = new(); } } diff --git a/Bloxstrap/UI/Menu/ViewModels/ModsViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/ModsViewModel.cs index 9be1101..e9117a1 100644 --- a/Bloxstrap/UI/Menu/ViewModels/ModsViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/ModsViewModel.cs @@ -1,7 +1,11 @@ -using System.ComponentModel; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Windows.Input; +using Bloxstrap.Enums; +using Bloxstrap.Extensions; + using CommunityToolkit.Mvvm.Input; namespace Bloxstrap.UI.Menu.ViewModels @@ -36,6 +40,14 @@ namespace Bloxstrap.UI.Menu.ViewModels set => App.Settings.Prop.UseDisableAppPatch = value; } + public IReadOnlyDictionary EmojiTypes => EmojiTypeEx.Selections; + + public string SelectedEmojiType + { + get => EmojiTypes.FirstOrDefault(x => x.Value == App.Settings.Prop.PreferredEmojiType).Key; + set => App.Settings.Prop.PreferredEmojiType = EmojiTypes[value]; + } + public bool DisableFullscreenOptimizationsEnabled { get => App.Settings.Prop.DisableFullscreenOptimizations; diff --git a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml index 8d0e4f1..57dd470 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml @@ -63,6 +63,7 @@ + @@ -107,6 +108,16 @@ + + + + + + + + + + From af26604980d115bbf39864d201fce048bb745803 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 18 May 2023 20:46:06 +0100 Subject: [PATCH 041/111] oh yeah some more stuff --- Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml index c423ae9..2a6f0bc 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml @@ -37,7 +37,8 @@ - + + @@ -54,6 +55,7 @@ + @@ -96,14 +98,19 @@ - EasternBloxxer + NikSavchenk0 - + + EasternBloxxer + + + + carter0nline - + From c7a7feeebfffd8131d368cdbc6253495ca5dc673 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 24 May 2023 11:02:57 +0100 Subject: [PATCH 042/111] Add process ID to log filename #212 an attempt to mitigate a bug with rogold accidentally launching roblox twice when launching a game through one of its added features making bloxstrap error because both instances will have the same log filename don't want to just terminate any launched instances that encounter the same filename since there could be a case of someone intentionally launching roblox repeatedly this won't actually stop the double launching, there's not much way around that without being able to see the executing args of other bloxstrap instances, but it solves the error --- Bloxstrap/App.xaml.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 34a229c..2dc51ab 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -80,8 +80,9 @@ namespace Bloxstrap bool isUsingTempDir = IsFirstRun || IsUninstall; string logdir = isUsingTempDir ? Path.Combine(Directories.LocalAppData, "Temp") : Path.Combine(Directories.Base, "Logs"); string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); + int processId = Process.GetCurrentProcess().Id; - Logger.Initialize(Path.Combine(logdir, $"{ProjectName}_{timestamp}.log")); + Logger.Initialize(Path.Combine(logdir, $"{ProjectName}_{timestamp}_{processId}.log")); // clean up any logs older than a week if (!isUsingTempDir) From 54b65f87283ba223e27dbaad00fc32d41b2a6d25 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 24 May 2023 11:21:53 +0100 Subject: [PATCH 043/111] Improve WebView2 install handling cuts down on code and should better account for multiple instances --- Bloxstrap/Bootstrapper.cs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 851642c..06dfbc2 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -73,7 +73,6 @@ namespace Bloxstrap private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); - private static bool ShouldInstallWebView2 = false; private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); @@ -96,15 +95,6 @@ namespace Bloxstrap public Bootstrapper(string launchCommandLine) { _launchCommandLine = launchCommandLine; - - // 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 - - string hklmLocation = "SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; - string hkcuLocation = "Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; - - ShouldInstallWebView2 = Registry.LocalMachine.OpenSubKey(hklmLocation) is null && Registry.CurrentUser.OpenSubKey(hkcuLocation) is null; } private void SetStatus(string message) @@ -192,8 +182,7 @@ namespace Bloxstrap MigrateIntegrations(); - if (ShouldInstallWebView2) - await InstallWebView2(); + await InstallWebView2(); App.FastFlags.Save(); await ApplyModifications(); @@ -804,7 +793,14 @@ namespace Bloxstrap private async Task InstallWebView2() { - if (!ShouldInstallWebView2) + // 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.LocalMachine.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); + + if (hklmKey is not null || hkcuKey is not null) return; App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Installing runtime..."); From 57e6454fd7153a7f6995ea11522481a7dea609ce Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 24 May 2023 14:56:47 +0100 Subject: [PATCH 044/111] Add program size estimation conditions are kinda messy but its fine --- Bloxstrap/Bootstrapper.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 06dfbc2..ab22d14 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -188,7 +188,10 @@ namespace Bloxstrap await ApplyModifications(); if (App.IsFirstRun || FreshInstall) + { Register(); + RegisterProgramSize(); + } CheckInstall(); @@ -425,6 +428,20 @@ namespace Bloxstrap App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); } + public void RegisterProgramSize() + { + App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); + + using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); + + // sum compressed and uncompressed package sizes and convert to kilobytes + int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; + + uninstallKey.SetValue("EstimatedSize", totalSize); + + App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); + } + private void CheckInstallMigration() { // check if we've changed our install location since the last time we started @@ -785,6 +802,10 @@ namespace Bloxstrap App.State.Prop.VersionGuid = _latestVersionGuid; + // don't register program size until the program is registered + if (!App.IsFirstRun && !FreshInstall) + RegisterProgramSize(); + if (Dialog is not null) Dialog.CancelEnabled = false; From 244c3dee40bef7147440ccc1ae6fdee8af0b73fd Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 24 May 2023 18:27:11 +0100 Subject: [PATCH 045/111] Fully cleanup Bloxstrap folder after install this also goes to show just how much of a mess bootstrapper dialog management is lol --- Bloxstrap/Bootstrapper.cs | 22 ++++++++++++++++++- .../IBootstrapperDialog.cs | 5 +++-- .../WPF/Views/ByfronDialog.xaml.cs | 6 ++++- .../WPF/Views/FluentDialog.xaml.cs | 6 ++++- .../WinForms/DialogBase.cs | 6 ++++- .../WinForms/VistaDialog.cs | 12 +++++++--- 6 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index ab22d14..55c6046 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -688,7 +688,27 @@ namespace Bloxstrap App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); } - Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled"); + Action? callback = null; + + if (Directory.Exists(Directories.Base)) + { + callback = () => + { + // 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" + + Process.Start(new ProcessStartInfo() + { + FileName = "cmd.exe", + Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + }; + } + + Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); } #endregion diff --git a/Bloxstrap/UI/BootstrapperDialogs/IBootstrapperDialog.cs b/Bloxstrap/UI/BootstrapperDialogs/IBootstrapperDialog.cs index 0531c1e..5b36844 100644 --- a/Bloxstrap/UI/BootstrapperDialogs/IBootstrapperDialog.cs +++ b/Bloxstrap/UI/BootstrapperDialogs/IBootstrapperDialog.cs @@ -1,4 +1,5 @@ -using System.Windows.Forms; +using System; +using System.Windows.Forms; namespace Bloxstrap.UI.BootstrapperDialogs { @@ -13,7 +14,7 @@ namespace Bloxstrap.UI.BootstrapperDialogs void ShowBootstrapper(); void CloseBootstrapper(); - void ShowSuccess(string message); + void ShowSuccess(string message, Action? callback = null); void ShowError(string message); void PromptShutdown(); } diff --git a/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/ByfronDialog.xaml.cs b/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/ByfronDialog.xaml.cs index 200327d..76c8c0c 100644 --- a/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/ByfronDialog.xaml.cs @@ -91,9 +91,13 @@ namespace Bloxstrap.UI.BootstrapperDialogs.WPF.Views public void CloseBootstrapper() => Dispatcher.BeginInvoke(this.Close); - public void ShowSuccess(string message) + public void ShowSuccess(string message, Action? callback) { App.ShowMessageBox(message, MessageBoxImage.Information); + + if (callback is not null) + callback(); + App.Terminate(); } diff --git a/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/FluentDialog.xaml.cs b/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/FluentDialog.xaml.cs index 052d7ba..559a2ab 100644 --- a/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/FluentDialog.xaml.cs +++ b/Bloxstrap/UI/BootstrapperDialogs/WPF/Views/FluentDialog.xaml.cs @@ -85,9 +85,13 @@ namespace Bloxstrap.UI.BootstrapperDialogs.WPF.Views // TODO: make prompts use dialog view natively rather than using message dialog boxes - public void ShowSuccess(string message) + public void ShowSuccess(string message, Action? callback) { App.ShowMessageBox(message, MessageBoxImage.Information); + + if (callback is not null) + callback(); + App.Terminate(); } diff --git a/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs b/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs index 3bec0eb..dfcc15e 100644 --- a/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs +++ b/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs @@ -99,9 +99,13 @@ namespace Bloxstrap.UI.BootstrapperDialogs.WinForms Close(); } - public virtual void ShowSuccess(string message) + public virtual void ShowSuccess(string message, Action? callback) { App.ShowMessageBox(message, MessageBoxImage.Information); + + if (callback is not null) + callback(); + App.Terminate(); } diff --git a/Bloxstrap/UI/BootstrapperDialogs/WinForms/VistaDialog.cs b/Bloxstrap/UI/BootstrapperDialogs/WinForms/VistaDialog.cs index d211101..211036f 100644 --- a/Bloxstrap/UI/BootstrapperDialogs/WinForms/VistaDialog.cs +++ b/Bloxstrap/UI/BootstrapperDialogs/WinForms/VistaDialog.cs @@ -80,11 +80,11 @@ namespace Bloxstrap.UI.BootstrapperDialogs.WinForms SetupDialog(); } - public override void ShowSuccess(string message) + public override void ShowSuccess(string message, Action? callback) { if (this.InvokeRequired) { - this.Invoke(ShowSuccess, message); + this.Invoke(ShowSuccess, message, callback); } else { @@ -96,7 +96,13 @@ namespace Bloxstrap.UI.BootstrapperDialogs.WinForms Buttons = { TaskDialogButton.OK } }; - successDialog.Buttons[0].Click += (_, _) => App.Terminate(); + successDialog.Buttons[0].Click += (_, _) => + { + if (callback is not null) + callback(); + + App.Terminate(); + }; _dialogPage.Navigate(successDialog); _dialogPage = successDialog; From ada0dc91af555da1228d28d68a1817d248f7fedc Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 25 May 2023 22:56:08 +0100 Subject: [PATCH 046/111] Better handling of malformed registry key (#264) --- Bloxstrap/App.xaml.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 2dc51ab..9e39d45 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -174,7 +174,12 @@ namespace Bloxstrap // check if installed using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}")) { - if (registryKey is null) + string? installLocation = null; + + if (registryKey is not null) + installLocation = (string?)registryKey.GetValue("InstallLocation"); + + if (registryKey is null || installLocation is null) { Logger.WriteLine("[App::OnStartup] Running first-time install"); @@ -191,7 +196,7 @@ namespace Bloxstrap else { IsFirstRun = false; - BaseDirectory = (string)registryKey.GetValue("InstallLocation")!; + BaseDirectory = installLocation; } } From c1410aa38ce38323fc46594d48dce6cabe6ad0c3 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 25 May 2023 23:02:53 +0100 Subject: [PATCH 047/111] Add option for FIB Phase 2 / ShadowMap (#263) --- Bloxstrap/Singletons/FastFlagManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index 7c5d839..0b2a7a4 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -32,7 +32,8 @@ namespace Bloxstrap.Singletons { { "Automatic", "" }, { "Voxel", "DFFlagDebugRenderForceTechnologyVoxel" }, - { "Future Is Bright", "FFlagDebugForceFutureIsBrightPhase3" } + { "ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" }, + { "Future", "FFlagDebugForceFutureIsBrightPhase3" } }; // this is one hell of a variable definition lmao From 1106d1ff0cfca1f8b07dc36dcb8a7bfa521d6afa Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 25 May 2023 23:09:29 +0100 Subject: [PATCH 048/111] Rearrange FastFlags menu tab prioritized presets that are likely to be used the most, and are what would stand out to people the most also removed the desktop app mobile layout preset, primarily just added it for filler when i was first working on fflag integration but now there really is too many options lol --- .../UI/Menu/ViewModels/FastFlagsViewModel.cs | 6 -- .../UI/Menu/Views/Pages/FastFlagsPage.xaml | 75 ++++++++----------- 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs index b4a175f..d5ccc5e 100644 --- a/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs @@ -142,11 +142,5 @@ namespace Bloxstrap.UI.Menu.ViewModels get => App.FastFlags.GetValue("DFIntCanHideGuiGroupId") == "32380007"; set => App.FastFlags.SetValue("DFIntCanHideGuiGroupId", value ? "32380007" : null); } - - public bool MobileLuaAppInterfaceEnabled - { - get => App.FastFlags.GetValue("FFlagLuaAppSystemBar") == "False"; - set => App.FastFlags.SetValue("FFlagLuaAppSystemBar", value ? "False" : null); - } } } diff --git a/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml index ec608b7..5977714 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml @@ -54,38 +54,11 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -96,15 +69,6 @@ - - - - - - - - - @@ -119,11 +83,38 @@ - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 053197ca64dc247dc0a23f3449fd702596e87b14 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 25 May 2023 23:13:36 +0100 Subject: [PATCH 049/111] Fix spacing --- Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml | 2 +- Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml b/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml index ceff752..b5b2c4c 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml @@ -12,7 +12,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml index 4ada611..e0d7ff4 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml @@ -11,7 +11,7 @@ - + From cf1514d6be6d0d9963984b595240d1dc1db07bc3 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 27 May 2023 19:25:33 +0100 Subject: [PATCH 050/111] Use ToLowerInvariant() instead of ToLower() (#268) oops --- Bloxstrap/Bootstrapper.cs | 6 +++--- Bloxstrap/ProtocolHandler.cs | 2 +- Bloxstrap/RobloxDeployment.cs | 4 ++-- Bloxstrap/ViewModels/InstallationViewModel.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 9ecfb0d..6392216 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -219,7 +219,7 @@ namespace Bloxstrap ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); // briefly check if current channel is suitable to use - if (App.Settings.Prop.Channel.ToLower() != RobloxDeployment.DefaultChannel.ToLower() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) { string? switchDefaultPrompt = null; ClientVersion? defaultChannelInfo = null; @@ -272,8 +272,8 @@ namespace Bloxstrap _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); - if (App.Settings.Prop.Channel.ToLower() != RobloxDeployment.DefaultChannel.ToLower()) - _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLower(); + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) + _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes bool shouldWait = false; diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/ProtocolHandler.cs index ffae5e5..a186c93 100644 --- a/Bloxstrap/ProtocolHandler.cs +++ b/Bloxstrap/ProtocolHandler.cs @@ -57,7 +57,7 @@ namespace Bloxstrap if (key == "channel") { - if (val.ToLower() != App.Settings.Prop.Channel.ToLower() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) + if (val.ToLowerInvariant() != App.Settings.Prop.Channel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) { MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox( $"{App.ProjectName} was launched with the Roblox build channel set to {val}, however your current preferred channel is {App.Settings.Prop.Channel}.\n\n" + diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index ed865c8..69390cc 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -78,8 +78,8 @@ namespace Bloxstrap string location = BaseUrl; - if (channel.ToLower() != DefaultChannel.ToLower()) - location += $"/channel/{channel.ToLower()}"; + if (channel.ToLowerInvariant() != DefaultChannel.ToLowerInvariant()) + location += $"/channel/{channel.ToLowerInvariant()}"; location += resource; diff --git a/Bloxstrap/ViewModels/InstallationViewModel.cs b/Bloxstrap/ViewModels/InstallationViewModel.cs index 7eca88e..ded5f5b 100644 --- a/Bloxstrap/ViewModels/InstallationViewModel.cs +++ b/Bloxstrap/ViewModels/InstallationViewModel.cs @@ -104,7 +104,7 @@ namespace Bloxstrap.ViewModels if (!value) { // roblox typically sets channels in all lowercase, so here we find if a case insensitive match exists - string? matchingChannel = Channels.Where(x => x.ToLower() == Channel.ToLower()).FirstOrDefault(); + string? matchingChannel = Channels.Where(x => x.ToLowerInvariant() == Channel.ToLowerInvariant()).FirstOrDefault(); Channel = String.IsNullOrEmpty(matchingChannel) ? RobloxDeployment.DefaultChannel : matchingChannel; } From 778a67dcb93c5537da5d78b6287eaca15e920ecf Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 12 Jun 2023 11:41:24 +0100 Subject: [PATCH 051/111] Add support for the 2023 v4 menu --- Bloxstrap/Properties/launchSettings.json | 4 +++ Bloxstrap/Singletons/FastFlagManager.cs | 35 ++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Bloxstrap/Properties/launchSettings.json b/Bloxstrap/Properties/launchSettings.json index 63472e5..ee3606b 100644 --- a/Bloxstrap/Properties/launchSettings.json +++ b/Bloxstrap/Properties/launchSettings.json @@ -18,6 +18,10 @@ "Bloxstrap (Menu)": { "commandName": "Project", "commandLineArgs": "-menu" + }, + "Bloxstrap (Deeplink)": { + "commandName": "Project", + "commandLineArgs": "roblox://experiences/start?placeId=95206881" } } } \ No newline at end of file diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index 0b2a7a4..191c645 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -36,7 +36,8 @@ namespace Bloxstrap.Singletons { "Future", "FFlagDebugForceFutureIsBrightPhase3" } }; - // this is one hell of a variable definition lmao + // 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> { { @@ -44,7 +45,10 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", null }, - { "FFlagEnableInGameMenuV3", null } + { "FFlagEnableInGameMenuV3", null }, + { "FFlagEnableInGameMenuControls", null }, + { "FFlagEnableV3MenuABTest3", null }, + { "FFlagEnableMenuControlsABTest", null } } }, @@ -53,7 +57,10 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", "True" }, - { "FFlagEnableInGameMenuV3", "False" } + { "FFlagEnableInGameMenuV3", "False" }, + { "FFlagEnableInGameMenuControls", "False" }, + { "FFlagEnableV3MenuABTest3", "False" }, + { "FFlagEnableMenuControlsABTest", "False" } } }, @@ -62,7 +69,10 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", "False" }, - { "FFlagEnableInGameMenuV3", "False" } + { "FFlagEnableInGameMenuV3", "False" }, + { "FFlagEnableInGameMenuControls", "False" }, + { "FFlagEnableV3MenuABTest3", "False" }, + { "FFlagEnableMenuControlsABTest", "False" } } }, @@ -71,7 +81,22 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", "False" }, - { "FFlagEnableInGameMenuV3", "True" } + { "FFlagEnableInGameMenuV3", "True" }, + { "FFlagEnableInGameMenuControls", "False" }, + { "FFlagEnableV3MenuABTest3", "False" }, + { "FFlagEnableMenuControlsABTest", "False" } + } + }, + + { + "Version 4 (2023)", + new Dictionary + { + { "FFlagDisableNewIGMinDUA", "True" }, + { "FFlagEnableInGameMenuV3", "False" }, + { "FFlagEnableInGameMenuControls", "True" }, + { "FFlagEnableV3MenuABTest3", "True" }, + { "FFlagEnableMenuControlsABTest", "True" } } } }; From 3926f308668583a0829f34efcde57a334f9465de Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 12 Jun 2023 12:16:42 +0100 Subject: [PATCH 052/111] Improve how multiple menu instances are handled --- Bloxstrap/App.xaml.cs | 23 ++++++++++------------- Bloxstrap/NativeMethods.cs | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 Bloxstrap/NativeMethods.cs diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 9e39d45..a16478d 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -228,24 +228,21 @@ namespace Bloxstrap if (IsMenuLaunch) { - Mutex mutex; + Process? menuProcess = Process.GetProcesses().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault(); - try + if (menuProcess is not null) { - mutex = Mutex.OpenExisting("Bloxstrap_MenuMutex"); - Logger.WriteLine("[App::OnStartup] Bloxstrap_MenuMutex mutex exists, aborting menu launch..."); - Terminate(); + IntPtr handle = menuProcess.MainWindowHandle; + Logger.WriteLine($"[App::OnStartup] Found an already existing menu window with handle {handle}"); + NativeMethods.SetForegroundWindow(handle); } - catch + else { - // no mutex exists, continue to opening preferences menu - mutex = new(true, "Bloxstrap_MenuMutex"); + if (Process.GetProcessesByName(ProjectName).Length > 1) + ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); + + new MainWindow().ShowDialog(); } - - if (Utilities.GetProcessCount(ProjectName) > 1) - ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); - - new MainWindow().ShowDialog(); } else if (LaunchArgs.Length > 0) { diff --git a/Bloxstrap/NativeMethods.cs b/Bloxstrap/NativeMethods.cs new file mode 100644 index 0000000..7cf6981 --- /dev/null +++ b/Bloxstrap/NativeMethods.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap +{ + static class NativeMethods + { + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); + } +} From 09ad91623643bd6b7b069980d2c0f65912ac8df3 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 12 Jun 2023 16:49:14 +0100 Subject: [PATCH 053/111] Improve how updating Roblox is handled fixes the problem of the versions folder not being properly cleaned out also instead of skipping the roblox update process if multiple instances are running, continue with the update process but don't delete any older versions when finished --- Bloxstrap/App.xaml.cs | 1 + Bloxstrap/Bootstrapper.cs | 34 +++++++++++++++++++--------------- Bloxstrap/Singletons/Logger.cs | 5 +++++ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index a16478d..d49ad31 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -30,6 +30,7 @@ namespace Bloxstrap public const string ProjectName = "Bloxstrap"; public const string ProjectRepository = "pizzaboxer/bloxstrap"; + public const string RobloxAppName = "RobloxPlayerBeta"; // used only for communicating between app and menu - use Directories.Base for anything else public static string BaseDirectory = null!; diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 3da54e1..7cf2a02 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -168,15 +168,10 @@ namespace Bloxstrap CheckInstallMigration(); - // only update roblox if we're running for the first time, or if - // roblox isn't running and our version guid is out of date, or the player exe doesn't exist - if (App.IsFirstRun || !Utilities.CheckIfRobloxRunning() && (_latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation))) + // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist + if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) await InstallLatestVersion(); - // last time the version folder was set, it was set to the latest version guid - // but if we skipped updating because roblox is already running, we want it to be set to our current version - _versionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); - if (App.IsFirstRun) App.ShouldSaveConfigs = true; @@ -792,19 +787,13 @@ namespace Bloxstrap { if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) { - App.Logger.WriteLine($"Deleting unused package {filename}"); + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); File.Delete(filename); } } string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); - if (_latestVersionGuid != App.State.Prop.VersionGuid && Directory.Exists(oldVersionFolder)) - { - // and also to delete our old version folder - Directory.Delete(oldVersionFolder, true); - } - // move old compatibility flags for the old location using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) { @@ -817,12 +806,27 @@ namespace Bloxstrap appFlagsKey.SetValue(_playerLocation, appFlags); appFlagsKey.DeleteValue(oldGameClientLocation); } + } + + // delete any old version folders + // we only do this if roblox isnt running just in case an update happened + // while they were launching a second instance or something idk + if (!Process.GetProcessesByName(App.RobloxAppName).Any()) + { + foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) + { + if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) + continue; + + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); + dir.Delete(true); + } } } App.State.Prop.VersionGuid = _latestVersionGuid; - // don't register program size until the program is registered + // don't register program size until the program is registered, which will be done after this if (!App.IsFirstRun && !FreshInstall) RegisterProgramSize(); diff --git a/Bloxstrap/Singletons/Logger.cs b/Bloxstrap/Singletons/Logger.cs index 97a5350..190f8b5 100644 --- a/Bloxstrap/Singletons/Logger.cs +++ b/Bloxstrap/Singletons/Logger.cs @@ -8,6 +8,11 @@ using System.Threading; namespace Bloxstrap.Singletons { // https://stackoverflow.com/a/53873141/11852173 + // TODO - this kind of sucks + // the main problem is just that this doesn't finish writing log entries before exiting the program + // this can be solved by making writetolog completely synchronous, but while it doesn't affect performance, its's not ideal + // also, writing and flushing for every single line that's written may not be great + public class Logger { private readonly SemaphoreSlim _semaphore = new(1, 1); From a71f8e5606f3f2c19adf96e3ea3c6ca74446d5b8 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 13 Jun 2023 08:12:16 +0100 Subject: [PATCH 054/111] WebView2 bugfix oops lol --- Bloxstrap/Bootstrapper.cs | 2438 ++++++++++++++++++------------------- 1 file changed, 1219 insertions(+), 1219 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 7cf2a02..7c5059b 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1,1224 +1,1224 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using System.Windows; - -using Microsoft.Win32; - -using Bloxstrap.Enums; -using Bloxstrap.Extensions; -using Bloxstrap.Integrations; -using Bloxstrap.Models; -using Bloxstrap.Tools; -using Bloxstrap.UI.BootstrapperDialogs; - -namespace Bloxstrap -{ - public class Bootstrapper - { - #region Properties - - // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes - public const int ERROR_SUCCESS = 0; - public const int ERROR_INSTALL_USEREXIT = 1602; - public const int ERROR_INSTALL_FAILURE = 1603; - - // in case a new package is added, you can find the corresponding directory - // by opening the stock bootstrapper in a hex editor - // TODO - there ideally should be a less static way to do this that's not hardcoded? - private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() - { - { "RobloxApp.zip", @"" }, - { "shaders.zip", @"shaders\" }, - { "ssl.zip", @"ssl\" }, - - // the runtime installer is only extracted if it needs installing - { "WebView2.zip", @"" }, - { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, - - { "content-avatar.zip", @"content\avatar\" }, - { "content-configs.zip", @"content\configs\" }, - { "content-fonts.zip", @"content\fonts\" }, - { "content-sky.zip", @"content\sky\" }, - { "content-sounds.zip", @"content\sounds\" }, - { "content-textures2.zip", @"content\textures\" }, - { "content-models.zip", @"content\models\" }, - - { "content-textures3.zip", @"PlatformContent\pc\textures\" }, - { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, - { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, - - { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, - { "extracontent-translations.zip", @"ExtraContent\translations\" }, - { "extracontent-models.zip", @"ExtraContent\models\" }, - { "extracontent-textures.zip", @"ExtraContent\textures\" }, - { "extracontent-places.zip", @"ExtraContent\places\" }, - }; - - private const string AppSettings = - "\r\n" + - "\r\n" + - " content\r\n" + - " http://www.roblox.com\r\n" + - "\r\n"; - - private readonly CancellationTokenSource _cancelTokenSource = new(); - - private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); - private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); - - private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); - - private string _launchCommandLine; - - private string _latestVersionGuid = null!; - private PackageManifest _versionPackageManifest = null!; - private string _versionFolder = null!; - - private bool _isInstalling = false; - private double _progressIncrement; - private long _totalDownloadedBytes = 0; - private int _packagesExtracted = 0; - private bool _cancelFired = false; - - public IBootstrapperDialog? Dialog = null; - #endregion - - #region Core - public Bootstrapper(string launchCommandLine) - { - _launchCommandLine = launchCommandLine; - } - - private void SetStatus(string message) - { - App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); - - // yea idk - if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) - message = message.Replace("...", ""); - - if (Dialog is not null) - Dialog.Message = message; - } - - private void UpdateProgressbar() - { - int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); - - // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 - // too lazy to fix properly so lol - if (newProgress > 100) - return; - - if (Dialog is not null) - Dialog.ProgressValue = newProgress; - } - - public async Task Run() - { - App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); - - if (App.IsUninstall) - { - Uninstall(); - return; - } - -#if !DEBUG - if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) - await CheckForUpdates(); -#endif - - // ensure only one instance of the bootstrapper is running at the time - // so that we don't have stuff like two updates happening simultaneously - - bool mutexExists = false; - - try - { - Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); - App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); - mutexExists = true; - } - catch (Exception) - { - // no mutex exists - } - - // wait for mutex to be released if it's not yet - await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex"); - await mutex.AcquireAsync(_cancelTokenSource.Token); - - // reload our configs since they've likely changed by now - if (mutexExists) - { - App.Settings.Load(); - App.State.Load(); - } - - await CheckLatestVersion(); - - CheckInstallMigration(); - - // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) - await InstallLatestVersion(); - - if (App.IsFirstRun) - App.ShouldSaveConfigs = true; - - MigrateIntegrations(); - - await InstallWebView2(); - - App.FastFlags.Save(); - await ApplyModifications(); - - if (App.IsFirstRun || FreshInstall) - { - Register(); - 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.IsNoLaunch) - Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed"); - else if (!App.IsNoLaunch && !_cancelFired) - await StartRoblox(); - } - - private async Task CheckLatestVersion() - { - SetStatus("Connecting to Roblox..."); - - ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); - - // briefly check if current channel is suitable to use - if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) - { - string? switchDefaultPrompt = null; - ClientVersion? defaultChannelInfo = null; - - App.Logger.WriteLine($"[Bootstrapper::CheckLatestVersion] Checking if current channel is suitable to use..."); - - if (String.IsNullOrEmpty(switchDefaultPrompt)) - { - // this SUCKS - defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - int defaultChannelVersion = Int32.Parse(defaultChannelInfo.Version.Split('.')[1]); - int currentChannelVersion = Int32.Parse(clientVersion.Version.Split('.')[1]); - - if (currentChannelVersion < defaultChannelVersion) - switchDefaultPrompt = $"Your current preferred channel ({App.Settings.Prop.Channel}) appears to no longer be receiving updates. Would you like to switch to {RobloxDeployment.DefaultChannel}?"; - } - - if (!String.IsNullOrEmpty(switchDefaultPrompt)) - { - MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(switchDefaultPrompt, MessageBoxImage.Question, MessageBoxButton.YesNo); - - if (result == MessageBoxResult.Yes) - { - App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; - App.Logger.WriteLine($"[DeployManager::SwitchToDefault] Changed Roblox release channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); - - if (defaultChannelInfo is null) - defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - - clientVersion = defaultChannelInfo; - } - } - } - - _latestVersionGuid = clientVersion.VersionGuid; - _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); - _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); - } - - private async Task StartRoblox() - { - SetStatus("Starting Roblox..."); - - if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) - { - Utilities.OpenWebsite("https://www.roblox.com/games"); - Dialog?.CloseBootstrapper(); - return; - } - - _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); - - if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) - _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); - - // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes - bool shouldWait = false; - - // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now - int gameClientPid; - using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) - { - gameClientPid = gameClient.Id; - } - - List autocloseProcesses = new(); - RobloxActivity? activityWatcher = null; - DiscordRichPresence? richPresence = null; - ServerNotifier? serverNotifier = null; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); - - using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) - { - bool startEventFired = await startEvent.WaitForEvent(); - - startEvent.Close(); - - if (!startEventFired) - return; - } - - if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) - { - activityWatcher = new(); - shouldWait = true; - } - - if (App.Settings.Prop.UseDiscordRichPresence) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); - richPresence = new(activityWatcher!); - } - - if (App.Settings.Prop.ShowServerDetails) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); - serverNotifier = new(activityWatcher!); - } - - // launch custom integrations now - foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); - - try - { - Process process = Process.Start(integration.Location, integration.LaunchArgs); - - if (integration.AutoClose) - { - shouldWait = true; - autocloseProcesses.Add(process); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); - } - } - - // event fired, wait for 3 seconds then close - await Task.Delay(3000); - Dialog?.CloseBootstrapper(); - - // keep bloxstrap open in the background if needed - if (!shouldWait) - return; - - activityWatcher?.StartWatcher(); - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); - - while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) - await Task.Delay(1000); - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); - - richPresence?.Dispose(); - - foreach (Process process in autocloseProcesses) - { - if (process.HasExited) - continue; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); - process.Kill(); - } - } - - public void CancelInstall() - { - if (!_isInstalling) - { - App.Terminate(ERROR_INSTALL_USEREXIT); - return; - } - - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); - - _cancelTokenSource.Cancel(); - _cancelFired = true; - - try - { - // clean up install - if (App.IsFirstRun) - Directory.Delete(Directories.Base, true); - else if (Directory.Exists(_versionFolder)) - Directory.Delete(_versionFolder, true); - } - catch (Exception ex) - { - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); - App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); - } - - App.Terminate(ERROR_INSTALL_USEREXIT); - } - #endregion - - #region App Install - public static void Register() - { - using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) - { - applicationKey.SetValue("InstallLocation", Directories.Base); - } - - // set uninstall key - using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) - { - uninstallKey.SetValue("DisplayIcon", $"{Directories.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", Directories.Base); - uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", "pizzaboxer"); - uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); - uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); - uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); - uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); - uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); - } - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); - } - - public void RegisterProgramSize() - { - App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); - - using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); - - // sum compressed and uncompressed package sizes and convert to kilobytes - int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; - - uninstallKey.SetValue("EstimatedSize", totalSize); - - App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); - } - - private void CheckInstallMigration() - { - // check if we've changed our install location since the last time we started - // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff - - using RegistryKey? applicationKey = Registry.CurrentUser.OpenSubKey($@"Software\{App.ProjectName}", true); - - string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); - - if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) - return; - - SetStatus("Migrating install location..."); - - if (Directory.Exists(oldInstallLocation)) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); - - foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) - { - string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); - string newFileLocation = Path.Combine(Directories.Base, relativeFile); - string? newDirectory = Path.GetDirectoryName(newFileLocation); - - try - { - if (!String.IsNullOrEmpty(newDirectory)) - Directory.CreateDirectory(newDirectory); - - File.Move(oldFileLocation, newFileLocation, true); - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); - } - } - - try - { - Directory.Delete(oldInstallLocation, true); - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); - } - } - - applicationKey.DeleteValue("OldInstallLocation"); - - // allow shortcuts to be re-registered - if (Directory.Exists(Directories.StartMenu)) - Directory.Delete(Directories.StartMenu, true); - - if (File.Exists(DesktopShortcutLocation)) - { - File.Delete(DesktopShortcutLocation); - App.Settings.Prop.CreateDesktopIcon = true; - } - - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); - } - - public static void CheckInstall() - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); - - // check if launch uri is set to our bootstrapper - // this doesn't go under register, so we check every launch - // just in case the stock bootstrapper changes it back - - ProtocolHandler.Register("roblox", "Roblox", Directories.Application); - ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); - - // in case the user is reinstalling - if (File.Exists(Directories.Application) && App.IsFirstRun) - File.Delete(Directories.Application); - - // check to make sure bootstrapper is in the install folder - if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) - File.Copy(Environment.ProcessPath, Directories.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(Directories.StartMenu)) - { - Directory.CreateDirectory(Directories.StartMenu); - - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); - - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); - } - else - { - // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" - string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); - string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); - - if (File.Exists(oldMenuShortcut)) - File.Delete(oldMenuShortcut); - - if (!File.Exists(newMenuShortcut)) - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(newMenuShortcut); - } - - if (App.Settings.Prop.CreateDesktopIcon) - { - if (!File.Exists(DesktopShortcutLocation)) - { - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(DesktopShortcutLocation); - } - - // one-time toggle, set it back to false - App.Settings.Prop.CreateDesktopIcon = false; - } - } - - private async Task CheckForUpdates() - { - // don't update if there's another instance running (likely running in the background) - if (Utilities.GetProcessCount(App.ProjectName) > 1) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); - return; - } - - string currentVersion = $"{App.ProjectName} v{App.Version}"; - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); - - var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - - if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); - return; - } - - SetStatus($"Getting the latest {App.ProjectName}..."); - - // 64-bit is always the first option - GithubReleaseAsset asset = releaseInfo.Assets[0]; - string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); - - if (!File.Exists(downloadLocation)) - { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - - await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - } - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); - - ProcessStartInfo startInfo = new() - { - FileName = downloadLocation, - }; - - foreach (string arg in App.LaunchArgs) - startInfo.ArgumentList.Add(arg); - - App.Settings.Save(); - - Process.Start(startInfo); - - Environment.Exit(0); - } - - private void Uninstall() - { - // prompt to shutdown roblox if its currently running - if (Utilities.CheckIfRobloxRunning()) - { - App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); - - Dialog?.PromptShutdown(); - - try - { - foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) - { - process.CloseMainWindow(); - process.Close(); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); - } - - App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); - } - - SetStatus($"Uninstalling {App.ProjectName}..."); - - //App.Settings.ShouldSave = false; - App.ShouldSaveConfigs = false; - - // check if stock bootstrapper is still installed - RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); - if (bootstrapperKey is null) - { - 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); - } - - try - { - // delete application key - Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"); - - // delete start menu folder - Directory.Delete(Directories.StartMenu, true); - - // delete desktop shortcut - File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); - - // delete uninstall key - Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); - - // delete installation folder - // (should delete everything except bloxstrap itself) - Directory.Delete(Directories.Base, true); - } - catch (Exception ex) - { - App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); - } - - Action? callback = null; - - if (Directory.Exists(Directories.Base)) - { - callback = () => - { - // 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" - - Process.Start(new ProcessStartInfo() - { - FileName = "cmd.exe", - Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - }; - } - - Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); - } - #endregion - - #region Roblox Install - private async Task InstallLatestVersion() - { - _isInstalling = true; - - SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); - - Directory.CreateDirectory(Directories.Base); - Directory.CreateDirectory(Directories.Downloads); - Directory.CreateDirectory(Directories.Versions); - - // package manifest states packed size and uncompressed size in exact bytes - // packed size only matters if we don't already have the package cached on disk - string[] cachedPackages = Directory.GetFiles(Directories.Downloads); - int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); - - if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) - { - App.ShowMessageBox($"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); - App.Terminate(ERROR_INSTALL_FAILURE); - return; - } - - if (Dialog is not null) - { - Dialog.CancelEnabled = true; - Dialog.ProgressStyle = ProgressBarStyle.Continuous; - } - - // compute total bytes to download - _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); - - foreach (Package package in _versionPackageManifest) - { - if (_cancelFired) - return; - - // download all the packages synchronously - await DownloadPackage(package); - - // we'll extract the runtime installer later if we need to - if (package.Name == "WebView2RuntimeInstaller.zip") - continue; - - // extract the package immediately after download asynchronously - // discard is just used to suppress the warning - Task _ = ExtractPackage(package); - } - - if (_cancelFired) - return; - - // allow progress bar to 100% before continuing (purely ux reasons lol) - await Task.Delay(1000); - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Marquee; - SetStatus("Configuring Roblox..."); - } - - // wait for all packages to finish extracting, with an exception for the webview2 runtime installer - while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) - { - await Task.Delay(100); - } - - string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); - await File.WriteAllTextAsync(appSettingsLocation, AppSettings); - - if (_cancelFired) - return; - - if (!FreshInstall) - { - // let's take this opportunity to delete any packages we don't need anymore - foreach (string filename in cachedPackages) - { - if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) - { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); - File.Delete(filename); - } - } - - string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); - - // move old compatibility flags for the old location - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); - string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); - - if (appFlags is not null) - { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); - appFlagsKey.SetValue(_playerLocation, appFlags); - appFlagsKey.DeleteValue(oldGameClientLocation); - } +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Windows; + +using Microsoft.Win32; + +using Bloxstrap.Enums; +using Bloxstrap.Extensions; +using Bloxstrap.Integrations; +using Bloxstrap.Models; +using Bloxstrap.Tools; +using Bloxstrap.UI.BootstrapperDialogs; + +namespace Bloxstrap +{ + public class Bootstrapper + { + #region Properties + + // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes + public const int ERROR_SUCCESS = 0; + public const int ERROR_INSTALL_USEREXIT = 1602; + public const int ERROR_INSTALL_FAILURE = 1603; + + // in case a new package is added, you can find the corresponding directory + // by opening the stock bootstrapper in a hex editor + // TODO - there ideally should be a less static way to do this that's not hardcoded? + private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() + { + { "RobloxApp.zip", @"" }, + { "shaders.zip", @"shaders\" }, + { "ssl.zip", @"ssl\" }, + + // the runtime installer is only extracted if it needs installing + { "WebView2.zip", @"" }, + { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, + + { "content-avatar.zip", @"content\avatar\" }, + { "content-configs.zip", @"content\configs\" }, + { "content-fonts.zip", @"content\fonts\" }, + { "content-sky.zip", @"content\sky\" }, + { "content-sounds.zip", @"content\sounds\" }, + { "content-textures2.zip", @"content\textures\" }, + { "content-models.zip", @"content\models\" }, + + { "content-textures3.zip", @"PlatformContent\pc\textures\" }, + { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, + { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, + + { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, + { "extracontent-translations.zip", @"ExtraContent\translations\" }, + { "extracontent-models.zip", @"ExtraContent\models\" }, + { "extracontent-textures.zip", @"ExtraContent\textures\" }, + { "extracontent-places.zip", @"ExtraContent\places\" }, + }; + + private const string AppSettings = + "\r\n" + + "\r\n" + + " content\r\n" + + " http://www.roblox.com\r\n" + + "\r\n"; + + private readonly CancellationTokenSource _cancelTokenSource = new(); + + private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); + private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); + + private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); + + private string _launchCommandLine; + + private string _latestVersionGuid = null!; + private PackageManifest _versionPackageManifest = null!; + private string _versionFolder = null!; + + private bool _isInstalling = false; + private double _progressIncrement; + private long _totalDownloadedBytes = 0; + private int _packagesExtracted = 0; + private bool _cancelFired = false; + + public IBootstrapperDialog? Dialog = null; + #endregion + + #region Core + public Bootstrapper(string launchCommandLine) + { + _launchCommandLine = launchCommandLine; + } + + private void SetStatus(string message) + { + App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); + + // yea idk + if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) + message = message.Replace("...", ""); + + if (Dialog is not null) + Dialog.Message = message; + } + + private void UpdateProgressbar() + { + int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); + + // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 + // too lazy to fix properly so lol + if (newProgress > 100) + return; + + if (Dialog is not null) + Dialog.ProgressValue = newProgress; + } + + public async Task Run() + { + App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); + + if (App.IsUninstall) + { + Uninstall(); + return; + } + +#if !DEBUG + if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) + await CheckForUpdates(); +#endif + + // ensure only one instance of the bootstrapper is running at the time + // so that we don't have stuff like two updates happening simultaneously + + bool mutexExists = false; + + try + { + Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); + App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); + mutexExists = true; + } + catch (Exception) + { + // no mutex exists + } + + // wait for mutex to be released if it's not yet + await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex"); + await mutex.AcquireAsync(_cancelTokenSource.Token); + + // reload our configs since they've likely changed by now + if (mutexExists) + { + App.Settings.Load(); + App.State.Load(); + } + + await CheckLatestVersion(); + + CheckInstallMigration(); + + // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist + if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) + await InstallLatestVersion(); + + if (App.IsFirstRun) + App.ShouldSaveConfigs = true; + + MigrateIntegrations(); + + await InstallWebView2(); + + App.FastFlags.Save(); + await ApplyModifications(); + + if (App.IsFirstRun || FreshInstall) + { + Register(); + 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.IsNoLaunch) + Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed"); + else if (!App.IsNoLaunch && !_cancelFired) + await StartRoblox(); + } + + private async Task CheckLatestVersion() + { + SetStatus("Connecting to Roblox..."); + + ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + + // briefly check if current channel is suitable to use + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) + { + string? switchDefaultPrompt = null; + ClientVersion? defaultChannelInfo = null; + + App.Logger.WriteLine($"[Bootstrapper::CheckLatestVersion] Checking if current channel is suitable to use..."); + + if (String.IsNullOrEmpty(switchDefaultPrompt)) + { + // this SUCKS + defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + int defaultChannelVersion = Int32.Parse(defaultChannelInfo.Version.Split('.')[1]); + int currentChannelVersion = Int32.Parse(clientVersion.Version.Split('.')[1]); + + if (currentChannelVersion < defaultChannelVersion) + switchDefaultPrompt = $"Your current preferred channel ({App.Settings.Prop.Channel}) appears to no longer be receiving updates. Would you like to switch to {RobloxDeployment.DefaultChannel}?"; + } + + if (!String.IsNullOrEmpty(switchDefaultPrompt)) + { + MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(switchDefaultPrompt, MessageBoxImage.Question, MessageBoxButton.YesNo); + + if (result == MessageBoxResult.Yes) + { + App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; + App.Logger.WriteLine($"[DeployManager::SwitchToDefault] Changed Roblox release channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); + + if (defaultChannelInfo is null) + defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + + clientVersion = defaultChannelInfo; + } + } + } + + _latestVersionGuid = clientVersion.VersionGuid; + _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); + _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); + } + + private async Task StartRoblox() + { + SetStatus("Starting Roblox..."); + + if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) + { + Utilities.OpenWebsite("https://www.roblox.com/games"); + Dialog?.CloseBootstrapper(); + return; + } + + _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); + + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) + _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); + + // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes + bool shouldWait = false; + + // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now + int gameClientPid; + using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) + { + gameClientPid = gameClient.Id; + } + + List autocloseProcesses = new(); + RobloxActivity? activityWatcher = null; + DiscordRichPresence? richPresence = null; + ServerNotifier? serverNotifier = null; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); + + using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) + { + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (!startEventFired) + return; + } + + if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) + { + activityWatcher = new(); + shouldWait = true; + } + + if (App.Settings.Prop.UseDiscordRichPresence) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); + richPresence = new(activityWatcher!); + } + + if (App.Settings.Prop.ShowServerDetails) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); + serverNotifier = new(activityWatcher!); + } + + // launch custom integrations now + foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); + + try + { + Process process = Process.Start(integration.Location, integration.LaunchArgs); + + if (integration.AutoClose) + { + shouldWait = true; + autocloseProcesses.Add(process); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); + } + } + + // event fired, wait for 3 seconds then close + await Task.Delay(3000); + Dialog?.CloseBootstrapper(); + + // keep bloxstrap open in the background if needed + if (!shouldWait) + return; + + activityWatcher?.StartWatcher(); + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); + + while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) + await Task.Delay(1000); + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); + + richPresence?.Dispose(); + + foreach (Process process in autocloseProcesses) + { + if (process.HasExited) + continue; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); + process.Kill(); + } + } + + public void CancelInstall() + { + if (!_isInstalling) + { + App.Terminate(ERROR_INSTALL_USEREXIT); + return; + } + + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); + + _cancelTokenSource.Cancel(); + _cancelFired = true; + + try + { + // clean up install + if (App.IsFirstRun) + Directory.Delete(Directories.Base, true); + else if (Directory.Exists(_versionFolder)) + Directory.Delete(_versionFolder, true); + } + catch (Exception ex) + { + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); + App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); + } + + App.Terminate(ERROR_INSTALL_USEREXIT); + } + #endregion + + #region App Install + public static void Register() + { + using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) + { + applicationKey.SetValue("InstallLocation", Directories.Base); + } + + // set uninstall key + using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) + { + uninstallKey.SetValue("DisplayIcon", $"{Directories.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", Directories.Base); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); + uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); + uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + } + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); + } + + public void RegisterProgramSize() + { + App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); + + using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); + + // sum compressed and uncompressed package sizes and convert to kilobytes + int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; + + uninstallKey.SetValue("EstimatedSize", totalSize); + + App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); + } + + private void CheckInstallMigration() + { + // check if we've changed our install location since the last time we started + // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff + + using RegistryKey? applicationKey = Registry.CurrentUser.OpenSubKey($@"Software\{App.ProjectName}", true); + + string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); + + if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) + return; + + SetStatus("Migrating install location..."); + + if (Directory.Exists(oldInstallLocation)) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); + + foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) + { + string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); + string newFileLocation = Path.Combine(Directories.Base, relativeFile); + string? newDirectory = Path.GetDirectoryName(newFileLocation); + + try + { + if (!String.IsNullOrEmpty(newDirectory)) + Directory.CreateDirectory(newDirectory); + + File.Move(oldFileLocation, newFileLocation, true); + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); + } + } + + try + { + Directory.Delete(oldInstallLocation, true); + App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); + } + } + + applicationKey.DeleteValue("OldInstallLocation"); + + // allow shortcuts to be re-registered + if (Directory.Exists(Directories.StartMenu)) + Directory.Delete(Directories.StartMenu, true); + + if (File.Exists(DesktopShortcutLocation)) + { + File.Delete(DesktopShortcutLocation); + App.Settings.Prop.CreateDesktopIcon = true; + } + + App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); + } + + public static void CheckInstall() + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); + + // check if launch uri is set to our bootstrapper + // this doesn't go under register, so we check every launch + // just in case the stock bootstrapper changes it back + + ProtocolHandler.Register("roblox", "Roblox", Directories.Application); + ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); + + // in case the user is reinstalling + if (File.Exists(Directories.Application) && App.IsFirstRun) + File.Delete(Directories.Application); + + // check to make sure bootstrapper is in the install folder + if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Directories.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(Directories.StartMenu)) + { + Directory.CreateDirectory(Directories.StartMenu); + + ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); + + ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); + } + else + { + // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" + string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); + string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); + + if (File.Exists(oldMenuShortcut)) + File.Delete(oldMenuShortcut); + + if (!File.Exists(newMenuShortcut)) + ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + .WriteToFile(newMenuShortcut); + } + + if (App.Settings.Prop.CreateDesktopIcon) + { + if (!File.Exists(DesktopShortcutLocation)) + { + ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + .WriteToFile(DesktopShortcutLocation); + } + + // one-time toggle, set it back to false + App.Settings.Prop.CreateDesktopIcon = false; + } + } + + private async Task CheckForUpdates() + { + // don't update if there's another instance running (likely running in the background) + if (Utilities.GetProcessCount(App.ProjectName) > 1) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); + return; + } + + string currentVersion = $"{App.ProjectName} v{App.Version}"; + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); + + var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); + + if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + return; + } + + SetStatus($"Getting the latest {App.ProjectName}..."); + + // 64-bit is always the first option + GithubReleaseAsset asset = releaseInfo.Assets[0]; + string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); + + if (!File.Exists(downloadLocation)) + { + var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + + await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + } + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); + + ProcessStartInfo startInfo = new() + { + FileName = downloadLocation, + }; + + foreach (string arg in App.LaunchArgs) + startInfo.ArgumentList.Add(arg); + + App.Settings.Save(); + + Process.Start(startInfo); + + Environment.Exit(0); + } + + private void Uninstall() + { + // prompt to shutdown roblox if its currently running + if (Utilities.CheckIfRobloxRunning()) + { + App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); + + Dialog?.PromptShutdown(); + + try + { + foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) + { + process.CloseMainWindow(); + process.Close(); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); + } + + App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); + } + + SetStatus($"Uninstalling {App.ProjectName}..."); + + //App.Settings.ShouldSave = false; + App.ShouldSaveConfigs = false; + + // check if stock bootstrapper is still installed + RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + if (bootstrapperKey is null) + { + 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); + } + + try + { + // delete application key + Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"); + + // delete start menu folder + Directory.Delete(Directories.StartMenu, true); + + // delete desktop shortcut + File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); + + // delete uninstall key + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); + + // delete installation folder + // (should delete everything except bloxstrap itself) + Directory.Delete(Directories.Base, true); + } + catch (Exception ex) + { + App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); + } + + Action? callback = null; + + if (Directory.Exists(Directories.Base)) + { + callback = () => + { + // 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" + + Process.Start(new ProcessStartInfo() + { + FileName = "cmd.exe", + Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + }; + } + + Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); + } + #endregion + + #region Roblox Install + private async Task InstallLatestVersion() + { + _isInstalling = true; + + SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); + + Directory.CreateDirectory(Directories.Base); + Directory.CreateDirectory(Directories.Downloads); + Directory.CreateDirectory(Directories.Versions); + + // package manifest states packed size and uncompressed size in exact bytes + // packed size only matters if we don't already have the package cached on disk + string[] cachedPackages = Directory.GetFiles(Directories.Downloads); + int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); + + if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) + { + App.ShowMessageBox($"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); + App.Terminate(ERROR_INSTALL_FAILURE); + return; + } + + if (Dialog is not null) + { + Dialog.CancelEnabled = true; + Dialog.ProgressStyle = ProgressBarStyle.Continuous; + } + + // compute total bytes to download + _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); + + foreach (Package package in _versionPackageManifest) + { + if (_cancelFired) + return; + + // download all the packages synchronously + await DownloadPackage(package); + + // we'll extract the runtime installer later if we need to + if (package.Name == "WebView2RuntimeInstaller.zip") + continue; + + // extract the package immediately after download asynchronously + // discard is just used to suppress the warning + Task _ = ExtractPackage(package); + } + + if (_cancelFired) + return; + + // allow progress bar to 100% before continuing (purely ux reasons lol) + await Task.Delay(1000); + + if (Dialog is not null) + { + Dialog.ProgressStyle = ProgressBarStyle.Marquee; + SetStatus("Configuring Roblox..."); + } + + // wait for all packages to finish extracting, with an exception for the webview2 runtime installer + while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) + { + await Task.Delay(100); + } + + string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); + await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + + if (_cancelFired) + return; + + if (!FreshInstall) + { + // let's take this opportunity to delete any packages we don't need anymore + foreach (string filename in cachedPackages) + { + if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) + { + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); + File.Delete(filename); + } + } + + string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); + + // move old compatibility flags for the old location + using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) + { + string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); + string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); + + if (appFlags is not null) + { + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); + appFlagsKey.SetValue(_playerLocation, appFlags); + appFlagsKey.DeleteValue(oldGameClientLocation); + } } // delete any old version folders // we only do this if roblox isnt running just in case an update happened // while they were launching a second instance or something idk - if (!Process.GetProcessesByName(App.RobloxAppName).Any()) - { - foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) - { - if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) - continue; - - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); - dir.Delete(true); - } - } - } - - App.State.Prop.VersionGuid = _latestVersionGuid; - - // don't register program size until the program is registered, which will be done after this - if (!App.IsFirstRun && !FreshInstall) - RegisterProgramSize(); - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - _isInstalling = false; - } - - private async Task 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.LocalMachine.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - - if (hklmKey is not null || hkcuKey is not null) - return; - - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Installing runtime..."); - - string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); - - if (!Directory.Exists(baseDirectory)) - { - Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); - - if (package is null) - { - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); - return; - } - - await ExtractPackage(package); - } - - SetStatus("Installing WebView2, please wait..."); - - ProcessStartInfo startInfo = new() - { - WorkingDirectory = baseDirectory, - FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), - Arguments = "/silent /install" - }; - - await Process.Start(startInfo)!.WaitForExitAsync(); - - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); - } - - public static void MigrateIntegrations() - { - // v2.2.0 - remove rbxfpsunlocker - string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); - - if (Directory.Exists(rbxfpsunlocker)) - Directory.Delete(rbxfpsunlocker, true); - - // v2.3.0 - remove reshade - string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); - string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); - - if (File.Exists(injectorLocation)) - { - App.ShowMessageBox( - "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ - "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", - MessageBoxImage.Warning - ); - - File.Delete(injectorLocation); - } - - if (File.Exists(configLocation)) - File.Delete(configLocation); - } - - private async Task ApplyModifications() - { - SetStatus("Applying Roblox modifications..."); - - // set executable flags for fullscreen optimizations - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - const string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; - string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); - - if (App.Settings.Prop.DisableFullscreenOptimizations) - { - if (appFlags is null) - appFlagsKey.SetValue(_playerLocation, $"~{flag}"); - else if (!appFlags.Contains(flag)) - appFlagsKey.SetValue(_playerLocation, appFlags + flag); - } - else if (appFlags is not null && appFlags.Contains(flag)) - { - // if there's more than one space, there's more flags set we need to preserve - if (appFlags.Split(' ').Length > 2) - appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); - else - appFlagsKey.DeleteValue(_playerLocation); - } - } - - // handle file mods - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); - string modFolder = Path.Combine(Directories.Modifications); - - // manifest has been moved to State.json - File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); - - List modFolderFiles = new(); - - if (!Directory.Exists(modFolder)) - Directory.CreateDirectory(modFolder); - - await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); - - // emoji presets are downloaded remotely from github due to how large they are - string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); - - if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) - { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - App.State.Prop.CurrentEmojiType = EmojiType.Default; - } - else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) - { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); - - var response = await App.HttpClient.GetAsync(remoteEmojiLocation); - await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - - App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; - } - - foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) - { - // get relative directory path - string relativeFile = file.Substring(modFolder.Length + 1); - - // v1.7.0 - README has been moved to the preferences menu now - if (relativeFile == "README.txt") - { - File.Delete(file); - continue; - } - - modFolderFiles.Add(relativeFile); - } - - // copy and overwrite - foreach (string file in modFolderFiles) - { - string fileModFolder = Path.Combine(modFolder, file); - string fileVersionFolder = Path.Combine(_versionFolder, file); - - if (File.Exists(fileVersionFolder)) - { - if (Utilities.MD5File(fileModFolder) == Utilities.MD5File(fileVersionFolder)) - continue; - } - - string? directory = Path.GetDirectoryName(fileVersionFolder); - - if (directory is null) - continue; - - Directory.CreateDirectory(directory); - - File.Copy(fileModFolder, fileVersionFolder, true); - File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); - } - - // the manifest is primarily here to keep track of what files have been - // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages - // now check for files that have been deleted from the mod folder according to the manifest - foreach (string fileLocation in App.State.Prop.ModManifest) - { - if (modFolderFiles.Contains(fileLocation)) - continue; - - KeyValuePair packageDirectory; - - try - { - packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); - } - catch (InvalidOperationException) - { - // package doesn't exist, likely mistakenly placed file - string versionFileLocation = Path.Combine(_versionFolder, fileLocation); - - if (File.Exists(versionFileLocation)) - File.Delete(versionFileLocation); - - continue; - } - - // restore original file - string fileName = fileLocation.Substring(packageDirectory.Value.Length); - ExtractFileFromPackage(packageDirectory.Key, fileName); - } - - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); - } - - private static async Task CheckModPreset(bool condition, string location, string name) - { - string modFolderLocation = Path.Combine(Directories.Modifications, location); - byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); - - if (condition) - { - if (!File.Exists(modFolderLocation)) - { - string? directory = Path.GetDirectoryName(modFolderLocation); - - if (directory is null) - return; - - Directory.CreateDirectory(directory); - - await File.WriteAllBytesAsync(modFolderLocation, binaryData); - } - } - else if (File.Exists(modFolderLocation) && Utilities.MD5File(modFolderLocation) == Utilities.MD5Data(binaryData)) - { - File.Delete(modFolderLocation); - } - } - - private async Task DownloadPackage(Package package) - { - if (_cancelFired) - return; - - string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); - - if (File.Exists(packageLocation)) - { - FileInfo file = new(packageLocation); - - string calculatedMD5 = Utilities.MD5File(packageLocation); - if (calculatedMD5 != package.Signature) - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); - file.Delete(); - } - else - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is already downloaded, skipping..."); - _totalDownloadedBytes += package.PackedSize; - UpdateProgressbar(); - return; - } - } - else if (File.Exists(robloxPackageLocation)) - { - // let's cheat! if the stock bootstrapper already previously downloaded the file, - // then we can just copy the one from there - - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, packageLocation); - _totalDownloadedBytes += package.PackedSize; - UpdateProgressbar(); - return; - } - - if (!File.Exists(packageLocation)) - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Downloading {package.Name} ({package.Signature})..."); - - { - var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); - var buffer = new byte[4096]; - - await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); - - while (true) - { - if (_cancelFired) - { - stream.Close(); - fileStream.Close(); - return; - } - - var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, _cancelTokenSource.Token); - - if (bytesRead == 0) - break; // we're done - - await fileStream.WriteAsync(buffer, 0, bytesRead, _cancelTokenSource.Token); - - _totalDownloadedBytes += bytesRead; - UpdateProgressbar(); - } - } - - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Finished downloading {package.Name}!"); - } - } - - private async Task ExtractPackage(Package package) - { - if (_cancelFired) - return; - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - string extractPath; - - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); - - using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation))) - { - foreach (ZipArchiveEntry entry in archive.Entries) - { - if (_cancelFired) - return; - - if (entry.FullName.EndsWith('\\')) - continue; - - extractPath = Path.Combine(packageFolder, entry.FullName); - - //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); - - string? directory = Path.GetDirectoryName(extractPath); - - if (directory is null) - continue; - - Directory.CreateDirectory(directory); - - await Task.Run(() => entry.ExtractToFile(extractPath, true)); - } - } - - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); - - _packagesExtracted += 1; - } - - private void ExtractFileFromPackage(string packageName, string fileName) - { - Package? package = _versionPackageManifest.Find(x => x.Name == packageName); - - if (package is null) - return; - - DownloadPackage(package).GetAwaiter().GetResult(); - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - - using ZipArchive archive = ZipFile.OpenRead(packageLocation); - - ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); - - if (entry is null) - return; - - string fileLocation = Path.Combine(packageFolder, entry.FullName); - - File.Delete(fileLocation); - - entry.ExtractToFile(fileLocation); - } - #endregion - } -} + if (!Process.GetProcessesByName(App.RobloxAppName).Any()) + { + foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) + { + if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) + continue; + + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); + dir.Delete(true); + } + } + } + + App.State.Prop.VersionGuid = _latestVersionGuid; + + // don't register program size until the program is registered, which will be done after this + if (!App.IsFirstRun && !FreshInstall) + RegisterProgramSize(); + + if (Dialog is not null) + Dialog.CancelEnabled = false; + + _isInstalling = false; + } + + private async Task 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($"[Bootstrapper::InstallWebView2] Installing runtime..."); + + string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); + + if (!Directory.Exists(baseDirectory)) + { + Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); + + if (package is null) + { + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); + return; + } + + await ExtractPackage(package); + } + + SetStatus("Installing WebView2, please wait..."); + + ProcessStartInfo startInfo = new() + { + WorkingDirectory = baseDirectory, + FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), + Arguments = "/silent /install" + }; + + await Process.Start(startInfo)!.WaitForExitAsync(); + + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); + } + + public static void MigrateIntegrations() + { + // v2.2.0 - remove rbxfpsunlocker + string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); + + if (Directory.Exists(rbxfpsunlocker)) + Directory.Delete(rbxfpsunlocker, true); + + // v2.3.0 - remove reshade + string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); + string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); + + if (File.Exists(injectorLocation)) + { + App.ShowMessageBox( + "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ + "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", + MessageBoxImage.Warning + ); + + File.Delete(injectorLocation); + } + + if (File.Exists(configLocation)) + File.Delete(configLocation); + } + + private async Task ApplyModifications() + { + SetStatus("Applying Roblox modifications..."); + + // set executable flags for fullscreen optimizations + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); + using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) + { + const string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; + string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); + + if (App.Settings.Prop.DisableFullscreenOptimizations) + { + if (appFlags is null) + appFlagsKey.SetValue(_playerLocation, $"~{flag}"); + else if (!appFlags.Contains(flag)) + appFlagsKey.SetValue(_playerLocation, appFlags + flag); + } + else if (appFlags is not null && appFlags.Contains(flag)) + { + // if there's more than one space, there's more flags set we need to preserve + if (appFlags.Split(' ').Length > 2) + appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); + else + appFlagsKey.DeleteValue(_playerLocation); + } + } + + // handle file mods + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); + string modFolder = Path.Combine(Directories.Modifications); + + // manifest has been moved to State.json + File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); + + List modFolderFiles = new(); + + if (!Directory.Exists(modFolder)) + Directory.CreateDirectory(modFolder); + + await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); + await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); + await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); + + // emoji presets are downloaded remotely from github due to how large they are + string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); + + if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + App.State.Prop.CurrentEmojiType = EmojiType.Default; + } + else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); + + var response = await App.HttpClient.GetAsync(remoteEmojiLocation); + await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + + App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; + } + + foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) + { + // get relative directory path + string relativeFile = file.Substring(modFolder.Length + 1); + + // v1.7.0 - README has been moved to the preferences menu now + if (relativeFile == "README.txt") + { + File.Delete(file); + continue; + } + + modFolderFiles.Add(relativeFile); + } + + // copy and overwrite + foreach (string file in modFolderFiles) + { + string fileModFolder = Path.Combine(modFolder, file); + string fileVersionFolder = Path.Combine(_versionFolder, file); + + if (File.Exists(fileVersionFolder)) + { + if (Utilities.MD5File(fileModFolder) == Utilities.MD5File(fileVersionFolder)) + continue; + } + + string? directory = Path.GetDirectoryName(fileVersionFolder); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + File.Copy(fileModFolder, fileVersionFolder, true); + File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); + } + + // the manifest is primarily here to keep track of what files have been + // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages + // now check for files that have been deleted from the mod folder according to the manifest + foreach (string fileLocation in App.State.Prop.ModManifest) + { + if (modFolderFiles.Contains(fileLocation)) + continue; + + KeyValuePair packageDirectory; + + try + { + packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + } + catch (InvalidOperationException) + { + // package doesn't exist, likely mistakenly placed file + string versionFileLocation = Path.Combine(_versionFolder, fileLocation); + + if (File.Exists(versionFileLocation)) + File.Delete(versionFileLocation); + + continue; + } + + // restore original file + string fileName = fileLocation.Substring(packageDirectory.Value.Length); + ExtractFileFromPackage(packageDirectory.Key, fileName); + } + + App.State.Prop.ModManifest = modFolderFiles; + App.State.Save(); + } + + private static async Task CheckModPreset(bool condition, string location, string name) + { + string modFolderLocation = Path.Combine(Directories.Modifications, location); + byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); + + if (condition) + { + if (!File.Exists(modFolderLocation)) + { + string? directory = Path.GetDirectoryName(modFolderLocation); + + if (directory is null) + return; + + Directory.CreateDirectory(directory); + + await File.WriteAllBytesAsync(modFolderLocation, binaryData); + } + } + else if (File.Exists(modFolderLocation) && Utilities.MD5File(modFolderLocation) == Utilities.MD5Data(binaryData)) + { + File.Delete(modFolderLocation); + } + } + + private async Task DownloadPackage(Package package) + { + if (_cancelFired) + return; + + string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); + + if (File.Exists(packageLocation)) + { + FileInfo file = new(packageLocation); + + string calculatedMD5 = Utilities.MD5File(packageLocation); + if (calculatedMD5 != package.Signature) + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); + file.Delete(); + } + else + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is already downloaded, skipping..."); + _totalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + } + else if (File.Exists(robloxPackageLocation)) + { + // let's cheat! if the stock bootstrapper already previously downloaded the file, + // then we can just copy the one from there + + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); + File.Copy(robloxPackageLocation, packageLocation); + _totalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + + if (!File.Exists(packageLocation)) + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Downloading {package.Name} ({package.Signature})..."); + + { + var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); + var buffer = new byte[4096]; + + await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); + await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); + + while (true) + { + if (_cancelFired) + { + stream.Close(); + fileStream.Close(); + return; + } + + var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, _cancelTokenSource.Token); + + if (bytesRead == 0) + break; // we're done + + await fileStream.WriteAsync(buffer, 0, bytesRead, _cancelTokenSource.Token); + + _totalDownloadedBytes += bytesRead; + UpdateProgressbar(); + } + } + + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Finished downloading {package.Name}!"); + } + } + + private async Task ExtractPackage(Package package) + { + if (_cancelFired) + return; + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + string extractPath; + + App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); + + using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation))) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (_cancelFired) + return; + + if (entry.FullName.EndsWith('\\')) + continue; + + extractPath = Path.Combine(packageFolder, entry.FullName); + + //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); + + string? directory = Path.GetDirectoryName(extractPath); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + await Task.Run(() => entry.ExtractToFile(extractPath, true)); + } + } + + App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); + + _packagesExtracted += 1; + } + + private void ExtractFileFromPackage(string packageName, string fileName) + { + Package? package = _versionPackageManifest.Find(x => x.Name == packageName); + + if (package is null) + return; + + DownloadPackage(package).GetAwaiter().GetResult(); + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + + using ZipArchive archive = ZipFile.OpenRead(packageLocation); + + ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); + + if (entry is null) + return; + + string fileLocation = Path.Combine(packageFolder, entry.FullName); + + File.Delete(fileLocation); + + entry.ExtractToFile(fileLocation); + } + #endregion + } +} From 59bda907d33ff1319a34e8a46d9d881e0f299219 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 13 Jun 2023 08:13:40 +0100 Subject: [PATCH 055/111] Revert "WebView2 bugfix" This reverts commit a71f8e5606f3f2c19adf96e3ea3c6ca74446d5b8. it was not supposed to do that :( whyyyyyyyyy --- Bloxstrap/Bootstrapper.cs | 2438 ++++++++++++++++++------------------- 1 file changed, 1219 insertions(+), 1219 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 7c5059b..7cf2a02 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1,1224 +1,1224 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using System.Windows; - -using Microsoft.Win32; - -using Bloxstrap.Enums; -using Bloxstrap.Extensions; -using Bloxstrap.Integrations; -using Bloxstrap.Models; -using Bloxstrap.Tools; -using Bloxstrap.UI.BootstrapperDialogs; - -namespace Bloxstrap -{ - public class Bootstrapper - { - #region Properties - - // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes - public const int ERROR_SUCCESS = 0; - public const int ERROR_INSTALL_USEREXIT = 1602; - public const int ERROR_INSTALL_FAILURE = 1603; - - // in case a new package is added, you can find the corresponding directory - // by opening the stock bootstrapper in a hex editor - // TODO - there ideally should be a less static way to do this that's not hardcoded? - private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() - { - { "RobloxApp.zip", @"" }, - { "shaders.zip", @"shaders\" }, - { "ssl.zip", @"ssl\" }, - - // the runtime installer is only extracted if it needs installing - { "WebView2.zip", @"" }, - { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, - - { "content-avatar.zip", @"content\avatar\" }, - { "content-configs.zip", @"content\configs\" }, - { "content-fonts.zip", @"content\fonts\" }, - { "content-sky.zip", @"content\sky\" }, - { "content-sounds.zip", @"content\sounds\" }, - { "content-textures2.zip", @"content\textures\" }, - { "content-models.zip", @"content\models\" }, - - { "content-textures3.zip", @"PlatformContent\pc\textures\" }, - { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, - { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, - - { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, - { "extracontent-translations.zip", @"ExtraContent\translations\" }, - { "extracontent-models.zip", @"ExtraContent\models\" }, - { "extracontent-textures.zip", @"ExtraContent\textures\" }, - { "extracontent-places.zip", @"ExtraContent\places\" }, - }; - - private const string AppSettings = - "\r\n" + - "\r\n" + - " content\r\n" + - " http://www.roblox.com\r\n" + - "\r\n"; - - private readonly CancellationTokenSource _cancelTokenSource = new(); - - private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); - private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); - - private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); - - private string _launchCommandLine; - - private string _latestVersionGuid = null!; - private PackageManifest _versionPackageManifest = null!; - private string _versionFolder = null!; - - private bool _isInstalling = false; - private double _progressIncrement; - private long _totalDownloadedBytes = 0; - private int _packagesExtracted = 0; - private bool _cancelFired = false; - - public IBootstrapperDialog? Dialog = null; - #endregion - - #region Core - public Bootstrapper(string launchCommandLine) - { - _launchCommandLine = launchCommandLine; - } - - private void SetStatus(string message) - { - App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); - - // yea idk - if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) - message = message.Replace("...", ""); - - if (Dialog is not null) - Dialog.Message = message; - } - - private void UpdateProgressbar() - { - int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); - - // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 - // too lazy to fix properly so lol - if (newProgress > 100) - return; - - if (Dialog is not null) - Dialog.ProgressValue = newProgress; - } - - public async Task Run() - { - App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); - - if (App.IsUninstall) - { - Uninstall(); - return; - } - -#if !DEBUG - if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) - await CheckForUpdates(); -#endif - - // ensure only one instance of the bootstrapper is running at the time - // so that we don't have stuff like two updates happening simultaneously - - bool mutexExists = false; - - try - { - Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); - App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); - mutexExists = true; - } - catch (Exception) - { - // no mutex exists - } - - // wait for mutex to be released if it's not yet - await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex"); - await mutex.AcquireAsync(_cancelTokenSource.Token); - - // reload our configs since they've likely changed by now - if (mutexExists) - { - App.Settings.Load(); - App.State.Load(); - } - - await CheckLatestVersion(); - - CheckInstallMigration(); - - // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) - await InstallLatestVersion(); - - if (App.IsFirstRun) - App.ShouldSaveConfigs = true; - - MigrateIntegrations(); - - await InstallWebView2(); - - App.FastFlags.Save(); - await ApplyModifications(); - - if (App.IsFirstRun || FreshInstall) - { - Register(); - 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.IsNoLaunch) - Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed"); - else if (!App.IsNoLaunch && !_cancelFired) - await StartRoblox(); - } - - private async Task CheckLatestVersion() - { - SetStatus("Connecting to Roblox..."); - - ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); - - // briefly check if current channel is suitable to use - if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) - { - string? switchDefaultPrompt = null; - ClientVersion? defaultChannelInfo = null; - - App.Logger.WriteLine($"[Bootstrapper::CheckLatestVersion] Checking if current channel is suitable to use..."); - - if (String.IsNullOrEmpty(switchDefaultPrompt)) - { - // this SUCKS - defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - int defaultChannelVersion = Int32.Parse(defaultChannelInfo.Version.Split('.')[1]); - int currentChannelVersion = Int32.Parse(clientVersion.Version.Split('.')[1]); - - if (currentChannelVersion < defaultChannelVersion) - switchDefaultPrompt = $"Your current preferred channel ({App.Settings.Prop.Channel}) appears to no longer be receiving updates. Would you like to switch to {RobloxDeployment.DefaultChannel}?"; - } - - if (!String.IsNullOrEmpty(switchDefaultPrompt)) - { - MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(switchDefaultPrompt, MessageBoxImage.Question, MessageBoxButton.YesNo); - - if (result == MessageBoxResult.Yes) - { - App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; - App.Logger.WriteLine($"[DeployManager::SwitchToDefault] Changed Roblox release channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); - - if (defaultChannelInfo is null) - defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - - clientVersion = defaultChannelInfo; - } - } - } - - _latestVersionGuid = clientVersion.VersionGuid; - _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); - _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); - } - - private async Task StartRoblox() - { - SetStatus("Starting Roblox..."); - - if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) - { - Utilities.OpenWebsite("https://www.roblox.com/games"); - Dialog?.CloseBootstrapper(); - return; - } - - _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); - - if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) - _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); - - // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes - bool shouldWait = false; - - // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now - int gameClientPid; - using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) - { - gameClientPid = gameClient.Id; - } - - List autocloseProcesses = new(); - RobloxActivity? activityWatcher = null; - DiscordRichPresence? richPresence = null; - ServerNotifier? serverNotifier = null; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); - - using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) - { - bool startEventFired = await startEvent.WaitForEvent(); - - startEvent.Close(); - - if (!startEventFired) - return; - } - - if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) - { - activityWatcher = new(); - shouldWait = true; - } - - if (App.Settings.Prop.UseDiscordRichPresence) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); - richPresence = new(activityWatcher!); - } - - if (App.Settings.Prop.ShowServerDetails) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); - serverNotifier = new(activityWatcher!); - } - - // launch custom integrations now - foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); - - try - { - Process process = Process.Start(integration.Location, integration.LaunchArgs); - - if (integration.AutoClose) - { - shouldWait = true; - autocloseProcesses.Add(process); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); - } - } - - // event fired, wait for 3 seconds then close - await Task.Delay(3000); - Dialog?.CloseBootstrapper(); - - // keep bloxstrap open in the background if needed - if (!shouldWait) - return; - - activityWatcher?.StartWatcher(); - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); - - while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) - await Task.Delay(1000); - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); - - richPresence?.Dispose(); - - foreach (Process process in autocloseProcesses) - { - if (process.HasExited) - continue; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); - process.Kill(); - } - } - - public void CancelInstall() - { - if (!_isInstalling) - { - App.Terminate(ERROR_INSTALL_USEREXIT); - return; - } - - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); - - _cancelTokenSource.Cancel(); - _cancelFired = true; - - try - { - // clean up install - if (App.IsFirstRun) - Directory.Delete(Directories.Base, true); - else if (Directory.Exists(_versionFolder)) - Directory.Delete(_versionFolder, true); - } - catch (Exception ex) - { - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); - App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); - } - - App.Terminate(ERROR_INSTALL_USEREXIT); - } - #endregion - - #region App Install - public static void Register() - { - using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) - { - applicationKey.SetValue("InstallLocation", Directories.Base); - } - - // set uninstall key - using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) - { - uninstallKey.SetValue("DisplayIcon", $"{Directories.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", Directories.Base); - uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", "pizzaboxer"); - uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); - uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); - uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); - uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); - uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); - } - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); - } - - public void RegisterProgramSize() - { - App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); - - using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); - - // sum compressed and uncompressed package sizes and convert to kilobytes - int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; - - uninstallKey.SetValue("EstimatedSize", totalSize); - - App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); - } - - private void CheckInstallMigration() - { - // check if we've changed our install location since the last time we started - // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff - - using RegistryKey? applicationKey = Registry.CurrentUser.OpenSubKey($@"Software\{App.ProjectName}", true); - - string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); - - if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) - return; - - SetStatus("Migrating install location..."); - - if (Directory.Exists(oldInstallLocation)) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); - - foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) - { - string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); - string newFileLocation = Path.Combine(Directories.Base, relativeFile); - string? newDirectory = Path.GetDirectoryName(newFileLocation); - - try - { - if (!String.IsNullOrEmpty(newDirectory)) - Directory.CreateDirectory(newDirectory); - - File.Move(oldFileLocation, newFileLocation, true); - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); - } - } - - try - { - Directory.Delete(oldInstallLocation, true); - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); - } - } - - applicationKey.DeleteValue("OldInstallLocation"); - - // allow shortcuts to be re-registered - if (Directory.Exists(Directories.StartMenu)) - Directory.Delete(Directories.StartMenu, true); - - if (File.Exists(DesktopShortcutLocation)) - { - File.Delete(DesktopShortcutLocation); - App.Settings.Prop.CreateDesktopIcon = true; - } - - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); - } - - public static void CheckInstall() - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); - - // check if launch uri is set to our bootstrapper - // this doesn't go under register, so we check every launch - // just in case the stock bootstrapper changes it back - - ProtocolHandler.Register("roblox", "Roblox", Directories.Application); - ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); - - // in case the user is reinstalling - if (File.Exists(Directories.Application) && App.IsFirstRun) - File.Delete(Directories.Application); - - // check to make sure bootstrapper is in the install folder - if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) - File.Copy(Environment.ProcessPath, Directories.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(Directories.StartMenu)) - { - Directory.CreateDirectory(Directories.StartMenu); - - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); - - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); - } - else - { - // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" - string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); - string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); - - if (File.Exists(oldMenuShortcut)) - File.Delete(oldMenuShortcut); - - if (!File.Exists(newMenuShortcut)) - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(newMenuShortcut); - } - - if (App.Settings.Prop.CreateDesktopIcon) - { - if (!File.Exists(DesktopShortcutLocation)) - { - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(DesktopShortcutLocation); - } - - // one-time toggle, set it back to false - App.Settings.Prop.CreateDesktopIcon = false; - } - } - - private async Task CheckForUpdates() - { - // don't update if there's another instance running (likely running in the background) - if (Utilities.GetProcessCount(App.ProjectName) > 1) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); - return; - } - - string currentVersion = $"{App.ProjectName} v{App.Version}"; - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); - - var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - - if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); - return; - } - - SetStatus($"Getting the latest {App.ProjectName}..."); - - // 64-bit is always the first option - GithubReleaseAsset asset = releaseInfo.Assets[0]; - string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); - - if (!File.Exists(downloadLocation)) - { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - - await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - } - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); - - ProcessStartInfo startInfo = new() - { - FileName = downloadLocation, - }; - - foreach (string arg in App.LaunchArgs) - startInfo.ArgumentList.Add(arg); - - App.Settings.Save(); - - Process.Start(startInfo); - - Environment.Exit(0); - } - - private void Uninstall() - { - // prompt to shutdown roblox if its currently running - if (Utilities.CheckIfRobloxRunning()) - { - App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); - - Dialog?.PromptShutdown(); - - try - { - foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) - { - process.CloseMainWindow(); - process.Close(); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); - } - - App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); - } - - SetStatus($"Uninstalling {App.ProjectName}..."); - - //App.Settings.ShouldSave = false; - App.ShouldSaveConfigs = false; - - // check if stock bootstrapper is still installed - RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); - if (bootstrapperKey is null) - { - 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); - } - - try - { - // delete application key - Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"); - - // delete start menu folder - Directory.Delete(Directories.StartMenu, true); - - // delete desktop shortcut - File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); - - // delete uninstall key - Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); - - // delete installation folder - // (should delete everything except bloxstrap itself) - Directory.Delete(Directories.Base, true); - } - catch (Exception ex) - { - App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); - } - - Action? callback = null; - - if (Directory.Exists(Directories.Base)) - { - callback = () => - { - // 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" - - Process.Start(new ProcessStartInfo() - { - FileName = "cmd.exe", - Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - }; - } - - Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); - } - #endregion - - #region Roblox Install - private async Task InstallLatestVersion() - { - _isInstalling = true; - - SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); - - Directory.CreateDirectory(Directories.Base); - Directory.CreateDirectory(Directories.Downloads); - Directory.CreateDirectory(Directories.Versions); - - // package manifest states packed size and uncompressed size in exact bytes - // packed size only matters if we don't already have the package cached on disk - string[] cachedPackages = Directory.GetFiles(Directories.Downloads); - int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); - - if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) - { - App.ShowMessageBox($"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); - App.Terminate(ERROR_INSTALL_FAILURE); - return; - } - - if (Dialog is not null) - { - Dialog.CancelEnabled = true; - Dialog.ProgressStyle = ProgressBarStyle.Continuous; - } - - // compute total bytes to download - _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); - - foreach (Package package in _versionPackageManifest) - { - if (_cancelFired) - return; - - // download all the packages synchronously - await DownloadPackage(package); - - // we'll extract the runtime installer later if we need to - if (package.Name == "WebView2RuntimeInstaller.zip") - continue; - - // extract the package immediately after download asynchronously - // discard is just used to suppress the warning - Task _ = ExtractPackage(package); - } - - if (_cancelFired) - return; - - // allow progress bar to 100% before continuing (purely ux reasons lol) - await Task.Delay(1000); - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Marquee; - SetStatus("Configuring Roblox..."); - } - - // wait for all packages to finish extracting, with an exception for the webview2 runtime installer - while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) - { - await Task.Delay(100); - } - - string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); - await File.WriteAllTextAsync(appSettingsLocation, AppSettings); - - if (_cancelFired) - return; - - if (!FreshInstall) - { - // let's take this opportunity to delete any packages we don't need anymore - foreach (string filename in cachedPackages) - { - if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) - { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); - File.Delete(filename); - } - } - - string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); - - // move old compatibility flags for the old location - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); - string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); - - if (appFlags is not null) - { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); - appFlagsKey.SetValue(_playerLocation, appFlags); - appFlagsKey.DeleteValue(oldGameClientLocation); - } +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Windows; + +using Microsoft.Win32; + +using Bloxstrap.Enums; +using Bloxstrap.Extensions; +using Bloxstrap.Integrations; +using Bloxstrap.Models; +using Bloxstrap.Tools; +using Bloxstrap.UI.BootstrapperDialogs; + +namespace Bloxstrap +{ + public class Bootstrapper + { + #region Properties + + // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes + public const int ERROR_SUCCESS = 0; + public const int ERROR_INSTALL_USEREXIT = 1602; + public const int ERROR_INSTALL_FAILURE = 1603; + + // in case a new package is added, you can find the corresponding directory + // by opening the stock bootstrapper in a hex editor + // TODO - there ideally should be a less static way to do this that's not hardcoded? + private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() + { + { "RobloxApp.zip", @"" }, + { "shaders.zip", @"shaders\" }, + { "ssl.zip", @"ssl\" }, + + // the runtime installer is only extracted if it needs installing + { "WebView2.zip", @"" }, + { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, + + { "content-avatar.zip", @"content\avatar\" }, + { "content-configs.zip", @"content\configs\" }, + { "content-fonts.zip", @"content\fonts\" }, + { "content-sky.zip", @"content\sky\" }, + { "content-sounds.zip", @"content\sounds\" }, + { "content-textures2.zip", @"content\textures\" }, + { "content-models.zip", @"content\models\" }, + + { "content-textures3.zip", @"PlatformContent\pc\textures\" }, + { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, + { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, + + { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, + { "extracontent-translations.zip", @"ExtraContent\translations\" }, + { "extracontent-models.zip", @"ExtraContent\models\" }, + { "extracontent-textures.zip", @"ExtraContent\textures\" }, + { "extracontent-places.zip", @"ExtraContent\places\" }, + }; + + private const string AppSettings = + "\r\n" + + "\r\n" + + " content\r\n" + + " http://www.roblox.com\r\n" + + "\r\n"; + + private readonly CancellationTokenSource _cancelTokenSource = new(); + + private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); + private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); + + private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); + + private string _launchCommandLine; + + private string _latestVersionGuid = null!; + private PackageManifest _versionPackageManifest = null!; + private string _versionFolder = null!; + + private bool _isInstalling = false; + private double _progressIncrement; + private long _totalDownloadedBytes = 0; + private int _packagesExtracted = 0; + private bool _cancelFired = false; + + public IBootstrapperDialog? Dialog = null; + #endregion + + #region Core + public Bootstrapper(string launchCommandLine) + { + _launchCommandLine = launchCommandLine; + } + + private void SetStatus(string message) + { + App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); + + // yea idk + if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) + message = message.Replace("...", ""); + + if (Dialog is not null) + Dialog.Message = message; + } + + private void UpdateProgressbar() + { + int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); + + // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 + // too lazy to fix properly so lol + if (newProgress > 100) + return; + + if (Dialog is not null) + Dialog.ProgressValue = newProgress; + } + + public async Task Run() + { + App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); + + if (App.IsUninstall) + { + Uninstall(); + return; + } + +#if !DEBUG + if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) + await CheckForUpdates(); +#endif + + // ensure only one instance of the bootstrapper is running at the time + // so that we don't have stuff like two updates happening simultaneously + + bool mutexExists = false; + + try + { + Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); + App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); + mutexExists = true; + } + catch (Exception) + { + // no mutex exists + } + + // wait for mutex to be released if it's not yet + await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex"); + await mutex.AcquireAsync(_cancelTokenSource.Token); + + // reload our configs since they've likely changed by now + if (mutexExists) + { + App.Settings.Load(); + App.State.Load(); + } + + await CheckLatestVersion(); + + CheckInstallMigration(); + + // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist + if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) + await InstallLatestVersion(); + + if (App.IsFirstRun) + App.ShouldSaveConfigs = true; + + MigrateIntegrations(); + + await InstallWebView2(); + + App.FastFlags.Save(); + await ApplyModifications(); + + if (App.IsFirstRun || FreshInstall) + { + Register(); + 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.IsNoLaunch) + Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed"); + else if (!App.IsNoLaunch && !_cancelFired) + await StartRoblox(); + } + + private async Task CheckLatestVersion() + { + SetStatus("Connecting to Roblox..."); + + ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + + // briefly check if current channel is suitable to use + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) + { + string? switchDefaultPrompt = null; + ClientVersion? defaultChannelInfo = null; + + App.Logger.WriteLine($"[Bootstrapper::CheckLatestVersion] Checking if current channel is suitable to use..."); + + if (String.IsNullOrEmpty(switchDefaultPrompt)) + { + // this SUCKS + defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + int defaultChannelVersion = Int32.Parse(defaultChannelInfo.Version.Split('.')[1]); + int currentChannelVersion = Int32.Parse(clientVersion.Version.Split('.')[1]); + + if (currentChannelVersion < defaultChannelVersion) + switchDefaultPrompt = $"Your current preferred channel ({App.Settings.Prop.Channel}) appears to no longer be receiving updates. Would you like to switch to {RobloxDeployment.DefaultChannel}?"; + } + + if (!String.IsNullOrEmpty(switchDefaultPrompt)) + { + MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(switchDefaultPrompt, MessageBoxImage.Question, MessageBoxButton.YesNo); + + if (result == MessageBoxResult.Yes) + { + App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; + App.Logger.WriteLine($"[DeployManager::SwitchToDefault] Changed Roblox release channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); + + if (defaultChannelInfo is null) + defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + + clientVersion = defaultChannelInfo; + } + } + } + + _latestVersionGuid = clientVersion.VersionGuid; + _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); + _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); + } + + private async Task StartRoblox() + { + SetStatus("Starting Roblox..."); + + if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) + { + Utilities.OpenWebsite("https://www.roblox.com/games"); + Dialog?.CloseBootstrapper(); + return; + } + + _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); + + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) + _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); + + // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes + bool shouldWait = false; + + // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now + int gameClientPid; + using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) + { + gameClientPid = gameClient.Id; + } + + List autocloseProcesses = new(); + RobloxActivity? activityWatcher = null; + DiscordRichPresence? richPresence = null; + ServerNotifier? serverNotifier = null; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); + + using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) + { + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (!startEventFired) + return; + } + + if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) + { + activityWatcher = new(); + shouldWait = true; + } + + if (App.Settings.Prop.UseDiscordRichPresence) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); + richPresence = new(activityWatcher!); + } + + if (App.Settings.Prop.ShowServerDetails) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); + serverNotifier = new(activityWatcher!); + } + + // launch custom integrations now + foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); + + try + { + Process process = Process.Start(integration.Location, integration.LaunchArgs); + + if (integration.AutoClose) + { + shouldWait = true; + autocloseProcesses.Add(process); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); + } + } + + // event fired, wait for 3 seconds then close + await Task.Delay(3000); + Dialog?.CloseBootstrapper(); + + // keep bloxstrap open in the background if needed + if (!shouldWait) + return; + + activityWatcher?.StartWatcher(); + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); + + while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) + await Task.Delay(1000); + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); + + richPresence?.Dispose(); + + foreach (Process process in autocloseProcesses) + { + if (process.HasExited) + continue; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); + process.Kill(); + } + } + + public void CancelInstall() + { + if (!_isInstalling) + { + App.Terminate(ERROR_INSTALL_USEREXIT); + return; + } + + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); + + _cancelTokenSource.Cancel(); + _cancelFired = true; + + try + { + // clean up install + if (App.IsFirstRun) + Directory.Delete(Directories.Base, true); + else if (Directory.Exists(_versionFolder)) + Directory.Delete(_versionFolder, true); + } + catch (Exception ex) + { + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); + App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); + } + + App.Terminate(ERROR_INSTALL_USEREXIT); + } + #endregion + + #region App Install + public static void Register() + { + using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) + { + applicationKey.SetValue("InstallLocation", Directories.Base); + } + + // set uninstall key + using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) + { + uninstallKey.SetValue("DisplayIcon", $"{Directories.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", Directories.Base); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); + uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); + uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + } + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); + } + + public void RegisterProgramSize() + { + App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); + + using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); + + // sum compressed and uncompressed package sizes and convert to kilobytes + int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; + + uninstallKey.SetValue("EstimatedSize", totalSize); + + App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); + } + + private void CheckInstallMigration() + { + // check if we've changed our install location since the last time we started + // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff + + using RegistryKey? applicationKey = Registry.CurrentUser.OpenSubKey($@"Software\{App.ProjectName}", true); + + string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); + + if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) + return; + + SetStatus("Migrating install location..."); + + if (Directory.Exists(oldInstallLocation)) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); + + foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) + { + string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); + string newFileLocation = Path.Combine(Directories.Base, relativeFile); + string? newDirectory = Path.GetDirectoryName(newFileLocation); + + try + { + if (!String.IsNullOrEmpty(newDirectory)) + Directory.CreateDirectory(newDirectory); + + File.Move(oldFileLocation, newFileLocation, true); + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); + } + } + + try + { + Directory.Delete(oldInstallLocation, true); + App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); + } + } + + applicationKey.DeleteValue("OldInstallLocation"); + + // allow shortcuts to be re-registered + if (Directory.Exists(Directories.StartMenu)) + Directory.Delete(Directories.StartMenu, true); + + if (File.Exists(DesktopShortcutLocation)) + { + File.Delete(DesktopShortcutLocation); + App.Settings.Prop.CreateDesktopIcon = true; + } + + App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); + } + + public static void CheckInstall() + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); + + // check if launch uri is set to our bootstrapper + // this doesn't go under register, so we check every launch + // just in case the stock bootstrapper changes it back + + ProtocolHandler.Register("roblox", "Roblox", Directories.Application); + ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); + + // in case the user is reinstalling + if (File.Exists(Directories.Application) && App.IsFirstRun) + File.Delete(Directories.Application); + + // check to make sure bootstrapper is in the install folder + if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Directories.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(Directories.StartMenu)) + { + Directory.CreateDirectory(Directories.StartMenu); + + ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); + + ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); + } + else + { + // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" + string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); + string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); + + if (File.Exists(oldMenuShortcut)) + File.Delete(oldMenuShortcut); + + if (!File.Exists(newMenuShortcut)) + ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + .WriteToFile(newMenuShortcut); + } + + if (App.Settings.Prop.CreateDesktopIcon) + { + if (!File.Exists(DesktopShortcutLocation)) + { + ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + .WriteToFile(DesktopShortcutLocation); + } + + // one-time toggle, set it back to false + App.Settings.Prop.CreateDesktopIcon = false; + } + } + + private async Task CheckForUpdates() + { + // don't update if there's another instance running (likely running in the background) + if (Utilities.GetProcessCount(App.ProjectName) > 1) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); + return; + } + + string currentVersion = $"{App.ProjectName} v{App.Version}"; + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); + + var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); + + if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + return; + } + + SetStatus($"Getting the latest {App.ProjectName}..."); + + // 64-bit is always the first option + GithubReleaseAsset asset = releaseInfo.Assets[0]; + string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); + + if (!File.Exists(downloadLocation)) + { + var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + + await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + } + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); + + ProcessStartInfo startInfo = new() + { + FileName = downloadLocation, + }; + + foreach (string arg in App.LaunchArgs) + startInfo.ArgumentList.Add(arg); + + App.Settings.Save(); + + Process.Start(startInfo); + + Environment.Exit(0); + } + + private void Uninstall() + { + // prompt to shutdown roblox if its currently running + if (Utilities.CheckIfRobloxRunning()) + { + App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); + + Dialog?.PromptShutdown(); + + try + { + foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) + { + process.CloseMainWindow(); + process.Close(); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); + } + + App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); + } + + SetStatus($"Uninstalling {App.ProjectName}..."); + + //App.Settings.ShouldSave = false; + App.ShouldSaveConfigs = false; + + // check if stock bootstrapper is still installed + RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + if (bootstrapperKey is null) + { + 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); + } + + try + { + // delete application key + Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"); + + // delete start menu folder + Directory.Delete(Directories.StartMenu, true); + + // delete desktop shortcut + File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); + + // delete uninstall key + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); + + // delete installation folder + // (should delete everything except bloxstrap itself) + Directory.Delete(Directories.Base, true); + } + catch (Exception ex) + { + App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); + } + + Action? callback = null; + + if (Directory.Exists(Directories.Base)) + { + callback = () => + { + // 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" + + Process.Start(new ProcessStartInfo() + { + FileName = "cmd.exe", + Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + }; + } + + Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); + } + #endregion + + #region Roblox Install + private async Task InstallLatestVersion() + { + _isInstalling = true; + + SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); + + Directory.CreateDirectory(Directories.Base); + Directory.CreateDirectory(Directories.Downloads); + Directory.CreateDirectory(Directories.Versions); + + // package manifest states packed size and uncompressed size in exact bytes + // packed size only matters if we don't already have the package cached on disk + string[] cachedPackages = Directory.GetFiles(Directories.Downloads); + int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); + + if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) + { + App.ShowMessageBox($"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); + App.Terminate(ERROR_INSTALL_FAILURE); + return; + } + + if (Dialog is not null) + { + Dialog.CancelEnabled = true; + Dialog.ProgressStyle = ProgressBarStyle.Continuous; + } + + // compute total bytes to download + _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); + + foreach (Package package in _versionPackageManifest) + { + if (_cancelFired) + return; + + // download all the packages synchronously + await DownloadPackage(package); + + // we'll extract the runtime installer later if we need to + if (package.Name == "WebView2RuntimeInstaller.zip") + continue; + + // extract the package immediately after download asynchronously + // discard is just used to suppress the warning + Task _ = ExtractPackage(package); + } + + if (_cancelFired) + return; + + // allow progress bar to 100% before continuing (purely ux reasons lol) + await Task.Delay(1000); + + if (Dialog is not null) + { + Dialog.ProgressStyle = ProgressBarStyle.Marquee; + SetStatus("Configuring Roblox..."); + } + + // wait for all packages to finish extracting, with an exception for the webview2 runtime installer + while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) + { + await Task.Delay(100); + } + + string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); + await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + + if (_cancelFired) + return; + + if (!FreshInstall) + { + // let's take this opportunity to delete any packages we don't need anymore + foreach (string filename in cachedPackages) + { + if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) + { + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); + File.Delete(filename); + } + } + + string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); + + // move old compatibility flags for the old location + using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) + { + string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); + string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); + + if (appFlags is not null) + { + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); + appFlagsKey.SetValue(_playerLocation, appFlags); + appFlagsKey.DeleteValue(oldGameClientLocation); + } } // delete any old version folders // we only do this if roblox isnt running just in case an update happened // while they were launching a second instance or something idk - if (!Process.GetProcessesByName(App.RobloxAppName).Any()) - { - foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) - { - if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) - continue; - - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); - dir.Delete(true); - } - } - } - - App.State.Prop.VersionGuid = _latestVersionGuid; - - // don't register program size until the program is registered, which will be done after this - if (!App.IsFirstRun && !FreshInstall) - RegisterProgramSize(); - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - _isInstalling = false; - } - - private async Task 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($"[Bootstrapper::InstallWebView2] Installing runtime..."); - - string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); - - if (!Directory.Exists(baseDirectory)) - { - Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); - - if (package is null) - { - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); - return; - } - - await ExtractPackage(package); - } - - SetStatus("Installing WebView2, please wait..."); - - ProcessStartInfo startInfo = new() - { - WorkingDirectory = baseDirectory, - FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), - Arguments = "/silent /install" - }; - - await Process.Start(startInfo)!.WaitForExitAsync(); - - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); - } - - public static void MigrateIntegrations() - { - // v2.2.0 - remove rbxfpsunlocker - string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); - - if (Directory.Exists(rbxfpsunlocker)) - Directory.Delete(rbxfpsunlocker, true); - - // v2.3.0 - remove reshade - string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); - string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); - - if (File.Exists(injectorLocation)) - { - App.ShowMessageBox( - "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ - "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", - MessageBoxImage.Warning - ); - - File.Delete(injectorLocation); - } - - if (File.Exists(configLocation)) - File.Delete(configLocation); - } - - private async Task ApplyModifications() - { - SetStatus("Applying Roblox modifications..."); - - // set executable flags for fullscreen optimizations - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - const string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; - string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); - - if (App.Settings.Prop.DisableFullscreenOptimizations) - { - if (appFlags is null) - appFlagsKey.SetValue(_playerLocation, $"~{flag}"); - else if (!appFlags.Contains(flag)) - appFlagsKey.SetValue(_playerLocation, appFlags + flag); - } - else if (appFlags is not null && appFlags.Contains(flag)) - { - // if there's more than one space, there's more flags set we need to preserve - if (appFlags.Split(' ').Length > 2) - appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); - else - appFlagsKey.DeleteValue(_playerLocation); - } - } - - // handle file mods - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); - string modFolder = Path.Combine(Directories.Modifications); - - // manifest has been moved to State.json - File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); - - List modFolderFiles = new(); - - if (!Directory.Exists(modFolder)) - Directory.CreateDirectory(modFolder); - - await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); - - // emoji presets are downloaded remotely from github due to how large they are - string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); - - if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) - { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - App.State.Prop.CurrentEmojiType = EmojiType.Default; - } - else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) - { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); - - var response = await App.HttpClient.GetAsync(remoteEmojiLocation); - await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - - App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; - } - - foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) - { - // get relative directory path - string relativeFile = file.Substring(modFolder.Length + 1); - - // v1.7.0 - README has been moved to the preferences menu now - if (relativeFile == "README.txt") - { - File.Delete(file); - continue; - } - - modFolderFiles.Add(relativeFile); - } - - // copy and overwrite - foreach (string file in modFolderFiles) - { - string fileModFolder = Path.Combine(modFolder, file); - string fileVersionFolder = Path.Combine(_versionFolder, file); - - if (File.Exists(fileVersionFolder)) - { - if (Utilities.MD5File(fileModFolder) == Utilities.MD5File(fileVersionFolder)) - continue; - } - - string? directory = Path.GetDirectoryName(fileVersionFolder); - - if (directory is null) - continue; - - Directory.CreateDirectory(directory); - - File.Copy(fileModFolder, fileVersionFolder, true); - File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); - } - - // the manifest is primarily here to keep track of what files have been - // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages - // now check for files that have been deleted from the mod folder according to the manifest - foreach (string fileLocation in App.State.Prop.ModManifest) - { - if (modFolderFiles.Contains(fileLocation)) - continue; - - KeyValuePair packageDirectory; - - try - { - packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); - } - catch (InvalidOperationException) - { - // package doesn't exist, likely mistakenly placed file - string versionFileLocation = Path.Combine(_versionFolder, fileLocation); - - if (File.Exists(versionFileLocation)) - File.Delete(versionFileLocation); - - continue; - } - - // restore original file - string fileName = fileLocation.Substring(packageDirectory.Value.Length); - ExtractFileFromPackage(packageDirectory.Key, fileName); - } - - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); - } - - private static async Task CheckModPreset(bool condition, string location, string name) - { - string modFolderLocation = Path.Combine(Directories.Modifications, location); - byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); - - if (condition) - { - if (!File.Exists(modFolderLocation)) - { - string? directory = Path.GetDirectoryName(modFolderLocation); - - if (directory is null) - return; - - Directory.CreateDirectory(directory); - - await File.WriteAllBytesAsync(modFolderLocation, binaryData); - } - } - else if (File.Exists(modFolderLocation) && Utilities.MD5File(modFolderLocation) == Utilities.MD5Data(binaryData)) - { - File.Delete(modFolderLocation); - } - } - - private async Task DownloadPackage(Package package) - { - if (_cancelFired) - return; - - string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); - - if (File.Exists(packageLocation)) - { - FileInfo file = new(packageLocation); - - string calculatedMD5 = Utilities.MD5File(packageLocation); - if (calculatedMD5 != package.Signature) - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); - file.Delete(); - } - else - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is already downloaded, skipping..."); - _totalDownloadedBytes += package.PackedSize; - UpdateProgressbar(); - return; - } - } - else if (File.Exists(robloxPackageLocation)) - { - // let's cheat! if the stock bootstrapper already previously downloaded the file, - // then we can just copy the one from there - - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, packageLocation); - _totalDownloadedBytes += package.PackedSize; - UpdateProgressbar(); - return; - } - - if (!File.Exists(packageLocation)) - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Downloading {package.Name} ({package.Signature})..."); - - { - var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); - var buffer = new byte[4096]; - - await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); - - while (true) - { - if (_cancelFired) - { - stream.Close(); - fileStream.Close(); - return; - } - - var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, _cancelTokenSource.Token); - - if (bytesRead == 0) - break; // we're done - - await fileStream.WriteAsync(buffer, 0, bytesRead, _cancelTokenSource.Token); - - _totalDownloadedBytes += bytesRead; - UpdateProgressbar(); - } - } - - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Finished downloading {package.Name}!"); - } - } - - private async Task ExtractPackage(Package package) - { - if (_cancelFired) - return; - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - string extractPath; - - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); - - using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation))) - { - foreach (ZipArchiveEntry entry in archive.Entries) - { - if (_cancelFired) - return; - - if (entry.FullName.EndsWith('\\')) - continue; - - extractPath = Path.Combine(packageFolder, entry.FullName); - - //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); - - string? directory = Path.GetDirectoryName(extractPath); - - if (directory is null) - continue; - - Directory.CreateDirectory(directory); - - await Task.Run(() => entry.ExtractToFile(extractPath, true)); - } - } - - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); - - _packagesExtracted += 1; - } - - private void ExtractFileFromPackage(string packageName, string fileName) - { - Package? package = _versionPackageManifest.Find(x => x.Name == packageName); - - if (package is null) - return; - - DownloadPackage(package).GetAwaiter().GetResult(); - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - - using ZipArchive archive = ZipFile.OpenRead(packageLocation); - - ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); - - if (entry is null) - return; - - string fileLocation = Path.Combine(packageFolder, entry.FullName); - - File.Delete(fileLocation); - - entry.ExtractToFile(fileLocation); - } - #endregion - } -} + if (!Process.GetProcessesByName(App.RobloxAppName).Any()) + { + foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) + { + if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) + continue; + + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); + dir.Delete(true); + } + } + } + + App.State.Prop.VersionGuid = _latestVersionGuid; + + // don't register program size until the program is registered, which will be done after this + if (!App.IsFirstRun && !FreshInstall) + RegisterProgramSize(); + + if (Dialog is not null) + Dialog.CancelEnabled = false; + + _isInstalling = false; + } + + private async Task 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.LocalMachine.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); + + if (hklmKey is not null || hkcuKey is not null) + return; + + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Installing runtime..."); + + string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); + + if (!Directory.Exists(baseDirectory)) + { + Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); + + if (package is null) + { + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); + return; + } + + await ExtractPackage(package); + } + + SetStatus("Installing WebView2, please wait..."); + + ProcessStartInfo startInfo = new() + { + WorkingDirectory = baseDirectory, + FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), + Arguments = "/silent /install" + }; + + await Process.Start(startInfo)!.WaitForExitAsync(); + + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); + } + + public static void MigrateIntegrations() + { + // v2.2.0 - remove rbxfpsunlocker + string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); + + if (Directory.Exists(rbxfpsunlocker)) + Directory.Delete(rbxfpsunlocker, true); + + // v2.3.0 - remove reshade + string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); + string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); + + if (File.Exists(injectorLocation)) + { + App.ShowMessageBox( + "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ + "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", + MessageBoxImage.Warning + ); + + File.Delete(injectorLocation); + } + + if (File.Exists(configLocation)) + File.Delete(configLocation); + } + + private async Task ApplyModifications() + { + SetStatus("Applying Roblox modifications..."); + + // set executable flags for fullscreen optimizations + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); + using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) + { + const string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; + string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); + + if (App.Settings.Prop.DisableFullscreenOptimizations) + { + if (appFlags is null) + appFlagsKey.SetValue(_playerLocation, $"~{flag}"); + else if (!appFlags.Contains(flag)) + appFlagsKey.SetValue(_playerLocation, appFlags + flag); + } + else if (appFlags is not null && appFlags.Contains(flag)) + { + // if there's more than one space, there's more flags set we need to preserve + if (appFlags.Split(' ').Length > 2) + appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); + else + appFlagsKey.DeleteValue(_playerLocation); + } + } + + // handle file mods + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); + string modFolder = Path.Combine(Directories.Modifications); + + // manifest has been moved to State.json + File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); + + List modFolderFiles = new(); + + if (!Directory.Exists(modFolder)) + Directory.CreateDirectory(modFolder); + + await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); + await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); + await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); + + // emoji presets are downloaded remotely from github due to how large they are + string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); + + if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + App.State.Prop.CurrentEmojiType = EmojiType.Default; + } + else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); + + var response = await App.HttpClient.GetAsync(remoteEmojiLocation); + await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + + App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; + } + + foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) + { + // get relative directory path + string relativeFile = file.Substring(modFolder.Length + 1); + + // v1.7.0 - README has been moved to the preferences menu now + if (relativeFile == "README.txt") + { + File.Delete(file); + continue; + } + + modFolderFiles.Add(relativeFile); + } + + // copy and overwrite + foreach (string file in modFolderFiles) + { + string fileModFolder = Path.Combine(modFolder, file); + string fileVersionFolder = Path.Combine(_versionFolder, file); + + if (File.Exists(fileVersionFolder)) + { + if (Utilities.MD5File(fileModFolder) == Utilities.MD5File(fileVersionFolder)) + continue; + } + + string? directory = Path.GetDirectoryName(fileVersionFolder); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + File.Copy(fileModFolder, fileVersionFolder, true); + File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); + } + + // the manifest is primarily here to keep track of what files have been + // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages + // now check for files that have been deleted from the mod folder according to the manifest + foreach (string fileLocation in App.State.Prop.ModManifest) + { + if (modFolderFiles.Contains(fileLocation)) + continue; + + KeyValuePair packageDirectory; + + try + { + packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + } + catch (InvalidOperationException) + { + // package doesn't exist, likely mistakenly placed file + string versionFileLocation = Path.Combine(_versionFolder, fileLocation); + + if (File.Exists(versionFileLocation)) + File.Delete(versionFileLocation); + + continue; + } + + // restore original file + string fileName = fileLocation.Substring(packageDirectory.Value.Length); + ExtractFileFromPackage(packageDirectory.Key, fileName); + } + + App.State.Prop.ModManifest = modFolderFiles; + App.State.Save(); + } + + private static async Task CheckModPreset(bool condition, string location, string name) + { + string modFolderLocation = Path.Combine(Directories.Modifications, location); + byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); + + if (condition) + { + if (!File.Exists(modFolderLocation)) + { + string? directory = Path.GetDirectoryName(modFolderLocation); + + if (directory is null) + return; + + Directory.CreateDirectory(directory); + + await File.WriteAllBytesAsync(modFolderLocation, binaryData); + } + } + else if (File.Exists(modFolderLocation) && Utilities.MD5File(modFolderLocation) == Utilities.MD5Data(binaryData)) + { + File.Delete(modFolderLocation); + } + } + + private async Task DownloadPackage(Package package) + { + if (_cancelFired) + return; + + string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); + + if (File.Exists(packageLocation)) + { + FileInfo file = new(packageLocation); + + string calculatedMD5 = Utilities.MD5File(packageLocation); + if (calculatedMD5 != package.Signature) + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); + file.Delete(); + } + else + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is already downloaded, skipping..."); + _totalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + } + else if (File.Exists(robloxPackageLocation)) + { + // let's cheat! if the stock bootstrapper already previously downloaded the file, + // then we can just copy the one from there + + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); + File.Copy(robloxPackageLocation, packageLocation); + _totalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + + if (!File.Exists(packageLocation)) + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Downloading {package.Name} ({package.Signature})..."); + + { + var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); + var buffer = new byte[4096]; + + await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); + await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); + + while (true) + { + if (_cancelFired) + { + stream.Close(); + fileStream.Close(); + return; + } + + var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, _cancelTokenSource.Token); + + if (bytesRead == 0) + break; // we're done + + await fileStream.WriteAsync(buffer, 0, bytesRead, _cancelTokenSource.Token); + + _totalDownloadedBytes += bytesRead; + UpdateProgressbar(); + } + } + + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Finished downloading {package.Name}!"); + } + } + + private async Task ExtractPackage(Package package) + { + if (_cancelFired) + return; + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + string extractPath; + + App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); + + using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation))) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (_cancelFired) + return; + + if (entry.FullName.EndsWith('\\')) + continue; + + extractPath = Path.Combine(packageFolder, entry.FullName); + + //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); + + string? directory = Path.GetDirectoryName(extractPath); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + await Task.Run(() => entry.ExtractToFile(extractPath, true)); + } + } + + App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); + + _packagesExtracted += 1; + } + + private void ExtractFileFromPackage(string packageName, string fileName) + { + Package? package = _versionPackageManifest.Find(x => x.Name == packageName); + + if (package is null) + return; + + DownloadPackage(package).GetAwaiter().GetResult(); + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + + using ZipArchive archive = ZipFile.OpenRead(packageLocation); + + ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); + + if (entry is null) + return; + + string fileLocation = Path.Combine(packageFolder, entry.FullName); + + File.Delete(fileLocation); + + entry.ExtractToFile(fileLocation); + } + #endregion + } +} From 324859521e742f69d7adfa7ab7f5b7d405156cd3 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 17:42:38 +0100 Subject: [PATCH 056/111] Line ending normalization --- Bloxstrap/Bootstrapper.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 7cf2a02..c98bd98 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -806,11 +806,11 @@ namespace Bloxstrap appFlagsKey.SetValue(_playerLocation, appFlags); appFlagsKey.DeleteValue(oldGameClientLocation); } - } - - // delete any old version folders - // we only do this if roblox isnt running just in case an update happened - // while they were launching a second instance or something idk + } + + // delete any old version folders + // we only do this if roblox isnt running just in case an update happened + // while they were launching a second instance or something idk if (!Process.GetProcessesByName(App.RobloxAppName).Any()) { foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) From 1232501698f84eff42bf2f0e4a46d2d651d10500 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 17:43:14 +0100 Subject: [PATCH 057/111] Fix WebView2 check --- Bloxstrap/Bootstrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index c98bd98..79f78f1 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -843,7 +843,7 @@ namespace Bloxstrap // 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.LocalMachine.OpenSubKey("Software\\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; From 9aded4ead5570ea9d59d24d727d617948d8b300d Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 20:43:48 +0100 Subject: [PATCH 058/111] Remove option for the 2021/v3 escape menu (#328) rip :( --- Bloxstrap/Singletons/FastFlagManager.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index 191c645..521b797 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -45,7 +45,6 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", null }, - { "FFlagEnableInGameMenuV3", null }, { "FFlagEnableInGameMenuControls", null }, { "FFlagEnableV3MenuABTest3", null }, { "FFlagEnableMenuControlsABTest", null } @@ -57,7 +56,6 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", "True" }, - { "FFlagEnableInGameMenuV3", "False" }, { "FFlagEnableInGameMenuControls", "False" }, { "FFlagEnableV3MenuABTest3", "False" }, { "FFlagEnableMenuControlsABTest", "False" } @@ -69,19 +67,6 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", "False" }, - { "FFlagEnableInGameMenuV3", "False" }, - { "FFlagEnableInGameMenuControls", "False" }, - { "FFlagEnableV3MenuABTest3", "False" }, - { "FFlagEnableMenuControlsABTest", "False" } - } - }, - - { - "Version 3 (2021)", - new Dictionary - { - { "FFlagDisableNewIGMinDUA", "False" }, - { "FFlagEnableInGameMenuV3", "True" }, { "FFlagEnableInGameMenuControls", "False" }, { "FFlagEnableV3MenuABTest3", "False" }, { "FFlagEnableMenuControlsABTest", "False" } @@ -93,7 +78,6 @@ namespace Bloxstrap.Singletons new Dictionary { { "FFlagDisableNewIGMinDUA", "True" }, - { "FFlagEnableInGameMenuV3", "False" }, { "FFlagEnableInGameMenuControls", "True" }, { "FFlagEnableV3MenuABTest3", "True" }, { "FFlagEnableMenuControlsABTest", "True" } From 14a18726744d9210bda9eb1363cc01c94ae5a08f Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 20:46:30 +0100 Subject: [PATCH 059/111] Disambiguate options for lighting mode selection --- Bloxstrap/Singletons/FastFlagManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index 521b797..788f90c 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -30,10 +30,10 @@ namespace Bloxstrap.Singletons public static IReadOnlyDictionary LightingTechnologies => new Dictionary { - { "Automatic", "" }, - { "Voxel", "DFFlagDebugRenderForceTechnologyVoxel" }, - { "ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" }, - { "Future", "FFlagDebugForceFutureIsBrightPhase3" } + { "Chosen by game", "" }, + { "Voxel (Phase 1)", "DFFlagDebugRenderForceTechnologyVoxel" }, + { "ShadowMap (Phase 2)", "FFlagDebugForceFutureIsBrightPhase2" }, + { "Future (Phase 3)", "FFlagDebugForceFutureIsBrightPhase3" } }; // this is one hell of a dictionary definition lmao From 670790a0236624144c808c7514d77b752f1d954e Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 21:36:07 +0100 Subject: [PATCH 060/111] Enable D3D exclusive fullscreen by default --- Bloxstrap/Singletons/FastFlagManager.cs | 51 +++++++------------ .../UI/Menu/ViewModels/FastFlagsViewModel.cs | 33 +++++------- .../UI/Menu/Views/Pages/FastFlagsPage.xaml | 9 ---- 3 files changed, 32 insertions(+), 61 deletions(-) diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/Singletons/FastFlagManager.cs index 788f90c..f0ee4d0 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/Singletons/FastFlagManager.cs @@ -22,9 +22,9 @@ namespace Bloxstrap.Singletons public static IReadOnlyDictionary RenderingModes => new Dictionary { { "Automatic", "" }, + { "Vulkan", "FFlagDebugGraphicsPreferVulkan" }, { "Direct3D 11", "FFlagDebugGraphicsPreferD3D11" }, { "Direct3D 10", "FFlagDebugGraphicsPreferD3D11FL10" }, - { "Vulkan", "FFlagDebugGraphicsPreferVulkan" }, { "OpenGL", "FFlagDebugGraphicsPreferOpenGL" } }; @@ -101,6 +101,22 @@ namespace Bloxstrap.Singletons } } + // this will set the flag to the corresponding value if the condition is true + // if the condition is not true, the flag will be erased + public void SetValueIf(bool condition, string key, object? value) + { + if (condition) + SetValue(key, value); + else if (GetValue(key) is not null) + SetValue(key, null); + } + + public void SetValueOnce(string key, object? value) + { + if (GetValue(key) is null) + SetValue(key, value); + } + // this returns null if the fflag doesn't exist public string? GetValue(string key) { @@ -114,42 +130,13 @@ namespace Bloxstrap.Singletons return null; } - public void SetRenderingMode(string value) - { - foreach (var mode in RenderingModes) - { - if (mode.Key != "Automatic") - SetValue(mode.Value, null); - } - - if (value != "Automatic") - SetValue(RenderingModes[value], "True"); - - if (value == "Vulkan") - SetValue("FFlagRenderVulkanFixMinimizeWindow", "True"); - else if (GetValue("FFlagRenderVulkanFixMinimizeWindow") is not null) - SetValue("FFlagRenderVulkanFixMinimizeWindow", null); - } - public override void Load() { base.Load(); // set to 9999 by default if it doesnt already exist - if (GetValue("DFIntTaskSchedulerTargetFps") is null) - SetValue("DFIntTaskSchedulerTargetFps", 9999); - - if (GetValue("FFlagDebugGraphicsPreferVulkan") == "True" && GetValue("FFlagRenderVulkanFixMinimizeWindow") is null) - SetValue("FFlagRenderVulkanFixMinimizeWindow", "True"); - - // exclusive fullscreen requires direct3d 10/11 to work - if (App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False") - { - if (!(App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11") == "True" || App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11FL10") == "True")) - { - SetRenderingMode("Direct3D 11"); - } - } + SetValueOnce("DFIntTaskSchedulerTargetFps", 9999); + SetValueOnce("FFlagHandleAltEnterFullscreenManually", "False"); } public override void Save() diff --git a/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs index d5ccc5e..ce1e301 100644 --- a/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs @@ -40,26 +40,19 @@ namespace Bloxstrap.UI.Menu.ViewModels return "Automatic"; } - set => App.FastFlags.SetRenderingMode(value); - } - - // this flag has to be set to false to work, weirdly enough - public bool ExclusiveFullscreenEnabled - { - get => App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False"; - set - { - App.FastFlags.SetValue("FFlagHandleAltEnterFullscreenManually", value ? "False" : null); - - if (value) - { - if (!(App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11") == "True" || App.FastFlags.GetValue("FFlagDebugGraphicsPreferD3D11FL10") == "True")) - { - App.FastFlags.SetRenderingMode("Direct3D 11"); - } - - OnPropertyChanged(nameof(SelectedRenderingMode)); - } + set + { + foreach (var mode in RenderingModes) + { + if (mode.Key != "Automatic") + App.FastFlags.SetValue(mode.Value, null); + } + + if (value == "Automatic") + return; + + App.FastFlags.SetValue(RenderingModes[value], "True"); + App.FastFlags.SetValueIf(value == "Vulkan", "FFlagRenderVulkanFixMinimizeWindow", "True"); } } diff --git a/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml index 5977714..d7a130e 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml @@ -98,15 +98,6 @@ - - - - - - - - - From 09d20ac42771f6bd227d61db0baebb94ad954bdb Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 21:50:47 +0100 Subject: [PATCH 061/111] Add installation path check (#314) --- .../UI/Menu/ViewModels/InstallationViewModel.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs index 0fd9fad..e3ca027 100644 --- a/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -65,11 +66,15 @@ namespace Bloxstrap.UI.Menu.ViewModels { using var dialog = new FolderBrowserDialog(); - if (dialog.ShowDialog() == DialogResult.OK) - { + if (dialog.ShowDialog() != DialogResult.OK) + return; + + if (!dialog.SelectedPath.EndsWith(App.ProjectName)) + InstallLocation = Path.Combine(dialog.SelectedPath, App.ProjectName); + else InstallLocation = dialog.SelectedPath; - OnPropertyChanged(nameof(InstallLocation)); - } + + OnPropertyChanged(nameof(InstallLocation)); } private void OpenFolder() From 28bcf57dff18c88255daff8c879e2aae6659cf43 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 22:06:40 +0100 Subject: [PATCH 062/111] Re-enable desktop app if launching with deeplink --- Bloxstrap/App.xaml.cs | 3 +++ Bloxstrap/Bootstrapper.cs | 2 +- Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index d49ad31..2890972 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -253,6 +253,9 @@ namespace Bloxstrap } else if (LaunchArgs[0].StartsWith("roblox:")) { + if (Settings.Prop.UseDisableAppPatch) + ShowMessageBox("Roblox was launched via a deeplink, however the desktop app is required for deeplink launching to work. Because you've opted to disable the desktop app, it will temporarily be re-enabled for this launch only.", MessageBoxImage.Information); + commandLine = $"--app --deeplink {LaunchArgs[0]}"; } else diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 79f78f1..a52d885 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -955,7 +955,7 @@ namespace Bloxstrap await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); + await CheckModPreset(App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"), @"ExtraContent\places\Mobile.rbxl", ""); // emoji presets are downloaded remotely from github due to how large they are string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); diff --git a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml index 57dd470..7c801c4 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml @@ -103,7 +103,7 @@ - + From e7fd0b964217e9ac338de8b5707210fe009076db Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 22:33:15 +0100 Subject: [PATCH 063/111] Minor restructuring --- Bloxstrap/App.xaml.cs | 4 +- Bloxstrap/Bootstrapper.cs | 13 +- Bloxstrap/{Singletons => }/FastFlagManager.cs | 18 +- Bloxstrap/{Singletons => }/JsonManager.cs | 2 +- Bloxstrap/{Singletons => }/Logger.cs | 2 +- .../WinForms/DialogBase.cs | 2 + .../UI/Menu/ViewModels/FastFlagsViewModel.cs | 35 ++- .../UI/Menu/ViewModels/GlobalViewModel.cs | 2 +- Bloxstrap/Updater.cs | 2 +- Bloxstrap/Utilities.cs | 60 +----- Bloxstrap/{Tools => Utility}/AsyncMutex.cs | 204 +++++++++--------- Bloxstrap/Utility/MD5Hash.cs | 34 +++ Bloxstrap/{ => Utility}/NativeMethods.cs | 4 +- Bloxstrap/{Tools => Utility}/SystemEvent.cs | 94 ++++---- Bloxstrap/{ => Utility}/WindowScaling.cs | 65 +++--- 15 files changed, 256 insertions(+), 285 deletions(-) rename Bloxstrap/{Singletons => }/FastFlagManager.cs (98%) rename Bloxstrap/{Singletons => }/JsonManager.cs (98%) rename Bloxstrap/{Singletons => }/Logger.cs (98%) rename Bloxstrap/{Tools => Utility}/AsyncMutex.cs (97%) create mode 100644 Bloxstrap/Utility/MD5Hash.cs rename Bloxstrap/{ => Utility}/NativeMethods.cs (91%) rename Bloxstrap/{Tools => Utility}/SystemEvent.cs (96%) rename Bloxstrap/{ => Utility}/WindowScaling.cs (78%) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 2890972..affaefa 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -15,10 +15,10 @@ using Microsoft.Win32; using Bloxstrap.Extensions; using Bloxstrap.Models; -using Bloxstrap.Singletons; using Bloxstrap.UI.BootstrapperDialogs; using Bloxstrap.UI.Menu.Views; - +using Bloxstrap.Utility; + namespace Bloxstrap { ///

diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index a52d885..66cfddc 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -256,7 +256,7 @@ namespace Bloxstrap if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) { - Utilities.OpenWebsite("https://www.roblox.com/games"); + Utilities.ShellExecute("https://www.roblox.com/games"); Dialog?.CloseBootstrapper(); return; } @@ -562,7 +562,7 @@ namespace Bloxstrap private async Task CheckForUpdates() { // don't update if there's another instance running (likely running in the background) - if (Utilities.GetProcessCount(App.ProjectName) > 1) + if (Process.GetProcessesByName(App.ProjectName).Count() > 1) { App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); return; @@ -616,7 +616,7 @@ namespace Bloxstrap private void Uninstall() { // prompt to shutdown roblox if its currently running - if (Utilities.CheckIfRobloxRunning()) + if (Process.GetProcessesByName(App.RobloxAppName).Any()) { App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); @@ -1004,7 +1004,7 @@ namespace Bloxstrap if (File.Exists(fileVersionFolder)) { - if (Utilities.MD5File(fileModFolder) == Utilities.MD5File(fileVersionFolder)) + if (Utility.MD5Hash.FromFile(fileModFolder) == Utility.MD5Hash.FromFile(fileVersionFolder)) continue; } @@ -1072,7 +1072,7 @@ namespace Bloxstrap await File.WriteAllBytesAsync(modFolderLocation, binaryData); } } - else if (File.Exists(modFolderLocation) && Utilities.MD5File(modFolderLocation) == Utilities.MD5Data(binaryData)) + else if (File.Exists(modFolderLocation) && Utility.MD5Hash.FromFile(modFolderLocation) == Utility.MD5Hash.FromBytes(binaryData)) { File.Delete(modFolderLocation); } @@ -1091,7 +1091,8 @@ namespace Bloxstrap { FileInfo file = new(packageLocation); - string calculatedMD5 = Utilities.MD5File(packageLocation); + string calculatedMD5 = Utility.MD5Hash.FromFile(packageLocation); + if (calculatedMD5 != package.Signature) { App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); diff --git a/Bloxstrap/Singletons/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs similarity index 98% rename from Bloxstrap/Singletons/FastFlagManager.cs rename to Bloxstrap/FastFlagManager.cs index f0ee4d0..104905e 100644 --- a/Bloxstrap/Singletons/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -1,10 +1,8 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Json; -namespace Bloxstrap.Singletons +namespace Bloxstrap { public class FastFlagManager : JsonManager> { @@ -103,18 +101,18 @@ namespace Bloxstrap.Singletons // this will set the flag to the corresponding value if the condition is true // if the condition is not true, the flag will be erased - public void SetValueIf(bool condition, string key, object? value) - { + public void SetValueIf(bool condition, string key, object? value) + { if (condition) SetValue(key, value); else if (GetValue(key) is not null) - SetValue(key, null); + SetValue(key, null); } - public void SetValueOnce(string key, object? value) - { + public void SetValueOnce(string key, object? value) + { if (GetValue(key) is null) - SetValue(key, value); + SetValue(key, value); } // this returns null if the fflag doesn't exist diff --git a/Bloxstrap/Singletons/JsonManager.cs b/Bloxstrap/JsonManager.cs similarity index 98% rename from Bloxstrap/Singletons/JsonManager.cs rename to Bloxstrap/JsonManager.cs index 7f212e8..f888341 100644 --- a/Bloxstrap/Singletons/JsonManager.cs +++ b/Bloxstrap/JsonManager.cs @@ -2,7 +2,7 @@ using System.IO; using System.Text.Json; -namespace Bloxstrap.Singletons +namespace Bloxstrap { public class JsonManager where T : new() { diff --git a/Bloxstrap/Singletons/Logger.cs b/Bloxstrap/Logger.cs similarity index 98% rename from Bloxstrap/Singletons/Logger.cs rename to Bloxstrap/Logger.cs index 190f8b5..ad4bc93 100644 --- a/Bloxstrap/Singletons/Logger.cs +++ b/Bloxstrap/Logger.cs @@ -5,7 +5,7 @@ using System.IO; using System.Text; using System.Threading; -namespace Bloxstrap.Singletons +namespace Bloxstrap { // https://stackoverflow.com/a/53873141/11852173 // TODO - this kind of sucks diff --git a/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs b/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs index dfcc15e..af9b34a 100644 --- a/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs +++ b/Bloxstrap/UI/BootstrapperDialogs/WinForms/DialogBase.cs @@ -1,7 +1,9 @@ using System; using System.Windows; using System.Windows.Forms; + using Bloxstrap.Extensions; +using Bloxstrap.Utility; namespace Bloxstrap.UI.BootstrapperDialogs.WinForms { diff --git a/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs index ce1e301..afb4a8c 100644 --- a/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/FastFlagsViewModel.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; -using Bloxstrap.Singletons; -using System.ComponentModel; - namespace Bloxstrap.UI.Menu.ViewModels { public class FastFlagsViewModel : INotifyPropertyChanged @@ -17,7 +14,7 @@ namespace Bloxstrap.UI.Menu.ViewModels public ICommand OpenClientSettingsCommand => new RelayCommand(OpenClientSettings); - private void OpenClientSettings() => Utilities.OpenWebsite(Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json")); + private void OpenClientSettings() => Utilities.ShellExecute(Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json")); public int FramerateLimit { @@ -40,19 +37,19 @@ namespace Bloxstrap.UI.Menu.ViewModels return "Automatic"; } - set - { - foreach (var mode in RenderingModes) - { - if (mode.Key != "Automatic") - App.FastFlags.SetValue(mode.Value, null); - } - - if (value == "Automatic") - return; - - App.FastFlags.SetValue(RenderingModes[value], "True"); - App.FastFlags.SetValueIf(value == "Vulkan", "FFlagRenderVulkanFixMinimizeWindow", "True"); + set + { + foreach (var mode in RenderingModes) + { + if (mode.Key != "Automatic") + App.FastFlags.SetValue(mode.Value, null); + } + + if (value == "Automatic") + return; + + App.FastFlags.SetValue(RenderingModes[value], "True"); + App.FastFlags.SetValueIf(value == "Vulkan", "FFlagRenderVulkanFixMinimizeWindow", "True"); } } diff --git a/Bloxstrap/UI/Menu/ViewModels/GlobalViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/GlobalViewModel.cs index 0b292a5..689945d 100644 --- a/Bloxstrap/UI/Menu/ViewModels/GlobalViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/GlobalViewModel.cs @@ -14,7 +14,7 @@ namespace Bloxstrap.UI.Menu.ViewModels if (location is null) return; - Utilities.OpenWebsite(location); + Utilities.ShellExecute(location); } } } diff --git a/Bloxstrap/Updater.cs b/Bloxstrap/Updater.cs index 5a7e979..d229f1c 100644 --- a/Bloxstrap/Updater.cs +++ b/Bloxstrap/Updater.cs @@ -78,7 +78,7 @@ namespace Bloxstrap if (isAutoUpgrade) { - EventHandler ReleaseNotesLauncher = new((_, _) => Utilities.OpenWebsite($"https://github.com/{App.ProjectRepository}/releases/tag/v{currentVersionInfo.ProductVersion}")); + EventHandler ReleaseNotesLauncher = new((_, _) => Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/releases/tag/v{currentVersionInfo.ProductVersion}")); App.Notification.BalloonTipTitle = $"Bloxstrap has been upgraded to v{currentVersionInfo.ProductVersion}"; App.Notification.BalloonTipText = "Click here to see what's new in this version"; diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 5cce944..53571d9 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -10,11 +10,6 @@ namespace Bloxstrap { static class Utilities { - public static bool IsDirectoryEmpty(string path) - { - return !Directory.EnumerateFileSystemEntries(path).Any(); - } - public static long GetFreeDiskSpace(string path) { foreach (DriveInfo drive in DriveInfo.GetDrives()) @@ -26,24 +21,7 @@ namespace Bloxstrap return -1; } - public static int GetProcessCount(string processName, bool log = true) - { - if (log) - App.Logger.WriteLine($"[Utilities::CheckIfProcessRunning] Checking if '{processName}' is running..."); - - Process[] processes = Process.GetProcessesByName(processName); - - if (log) - App.Logger.WriteLine($"[Utilities::CheckIfProcessRunning] Found {processes.Length} process(es) running for '{processName}'"); - - return processes.Length; - } - - public static bool CheckIfProcessRunning(string processName, bool log = true) => GetProcessCount(processName, log) >= 1; - - public static bool CheckIfRobloxRunning(bool log = true) => CheckIfProcessRunning("RobloxPlayerBeta", log); - - public static void OpenWebsite(string website) => Process.Start(new ProcessStartInfo { FileName = website, UseShellExecute = true }); + public static void ShellExecute(string website) => Process.Start(new ProcessStartInfo { FileName = website, UseShellExecute = true }); public static async Task GetJson(string url) { @@ -61,41 +39,5 @@ namespace Bloxstrap return default; } } - - public static string MD5File(string filename) - { - using (MD5 md5 = MD5.Create()) - { - using (FileStream stream = File.OpenRead(filename)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - - public static string MD5Data(byte[] data) - { - using (MD5 md5 = MD5.Create()) - { - byte[] hash = md5.ComputeHash(data); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - - // quick and hacky way of getting a value from any key/value pair formatted list - // (command line args, uri params, etc) - public static string? GetKeyValue(string subject, string key, char delimiter) - { - if (subject.LastIndexOf(key) == -1) - return null; - - string substr = subject.Substring(subject.LastIndexOf(key) + key.Length); - - if (!substr.Contains(delimiter)) - return null; - - return substr.Split(delimiter)[0]; - } } } diff --git a/Bloxstrap/Tools/AsyncMutex.cs b/Bloxstrap/Utility/AsyncMutex.cs similarity index 97% rename from Bloxstrap/Tools/AsyncMutex.cs rename to Bloxstrap/Utility/AsyncMutex.cs index fa6d137..e6e972f 100644 --- a/Bloxstrap/Tools/AsyncMutex.cs +++ b/Bloxstrap/Utility/AsyncMutex.cs @@ -1,102 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Bloxstrap.Tools -{ - // https://gist.github.com/dfederm/35c729f6218834b764fa04c219181e4e - - public sealed class AsyncMutex : IAsyncDisposable - { - private readonly string _name; - private Task? _mutexTask; - private ManualResetEventSlim? _releaseEvent; - private CancellationTokenSource? _cancellationTokenSource; - - public AsyncMutex(string name) - { - _name = name; - } - - public Task AcquireAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - TaskCompletionSource taskCompletionSource = new(); - - _releaseEvent = new ManualResetEventSlim(); - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - // Putting all mutex manipulation in its own task as it doesn't work in async contexts - // Note: this task should not throw. - _mutexTask = Task.Factory.StartNew( - state => - { - try - { - CancellationToken cancellationToken = _cancellationTokenSource.Token; - using var mutex = new Mutex(false, _name); - try - { - // Wait for either the mutex to be acquired, or cancellation - if (WaitHandle.WaitAny(new[] { mutex, cancellationToken.WaitHandle }) != 0) - { - taskCompletionSource.SetCanceled(cancellationToken); - return; - } - } - catch (AbandonedMutexException) - { - // Abandoned by another process, we acquired it. - } - - taskCompletionSource.SetResult(); - - // Wait until the release call - _releaseEvent.Wait(); - - mutex.ReleaseMutex(); - } - catch (OperationCanceledException) - { - taskCompletionSource.TrySetCanceled(cancellationToken); - } - catch (Exception ex) - { - taskCompletionSource.TrySetException(ex); - } - }, - state: null, - cancellationToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - return taskCompletionSource.Task; - } - - public async Task ReleaseAsync() - { - _releaseEvent?.Set(); - - if (_mutexTask != null) - { - await _mutexTask; - } - } - - public async ValueTask DisposeAsync() - { - // Ensure the mutex task stops waiting for any acquire - _cancellationTokenSource?.Cancel(); - - // Ensure the mutex is released - await ReleaseAsync(); - - _releaseEvent?.Dispose(); - _cancellationTokenSource?.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Bloxstrap.Tools +{ + // https://gist.github.com/dfederm/35c729f6218834b764fa04c219181e4e + + public sealed class AsyncMutex : IAsyncDisposable + { + private readonly string _name; + private Task? _mutexTask; + private ManualResetEventSlim? _releaseEvent; + private CancellationTokenSource? _cancellationTokenSource; + + public AsyncMutex(string name) + { + _name = name; + } + + public Task AcquireAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + TaskCompletionSource taskCompletionSource = new(); + + _releaseEvent = new ManualResetEventSlim(); + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + // Putting all mutex manipulation in its own task as it doesn't work in async contexts + // Note: this task should not throw. + _mutexTask = Task.Factory.StartNew( + state => + { + try + { + CancellationToken cancellationToken = _cancellationTokenSource.Token; + using var mutex = new Mutex(false, _name); + try + { + // Wait for either the mutex to be acquired, or cancellation + if (WaitHandle.WaitAny(new[] { mutex, cancellationToken.WaitHandle }) != 0) + { + taskCompletionSource.SetCanceled(cancellationToken); + return; + } + } + catch (AbandonedMutexException) + { + // Abandoned by another process, we acquired it. + } + + taskCompletionSource.SetResult(); + + // Wait until the release call + _releaseEvent.Wait(); + + mutex.ReleaseMutex(); + } + catch (OperationCanceledException) + { + taskCompletionSource.TrySetCanceled(cancellationToken); + } + catch (Exception ex) + { + taskCompletionSource.TrySetException(ex); + } + }, + state: null, + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + return taskCompletionSource.Task; + } + + public async Task ReleaseAsync() + { + _releaseEvent?.Set(); + + if (_mutexTask != null) + { + await _mutexTask; + } + } + + public async ValueTask DisposeAsync() + { + // Ensure the mutex task stops waiting for any acquire + _cancellationTokenSource?.Cancel(); + + // Ensure the mutex is released + await ReleaseAsync(); + + _releaseEvent?.Dispose(); + _cancellationTokenSource?.Dispose(); + } + } +} diff --git a/Bloxstrap/Utility/MD5Hash.cs b/Bloxstrap/Utility/MD5Hash.cs new file mode 100644 index 0000000..9e1abcb --- /dev/null +++ b/Bloxstrap/Utility/MD5Hash.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.Utility +{ + public static class MD5Hash + { + public static string FromFile(string filename) + { + using (MD5 md5 = MD5.Create()) + { + using (FileStream stream = File.OpenRead(filename)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + + public static string FromBytes(byte[] data) + { + using (MD5 md5 = MD5.Create()) + { + byte[] hash = md5.ComputeHash(data); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } +} diff --git a/Bloxstrap/NativeMethods.cs b/Bloxstrap/Utility/NativeMethods.cs similarity index 91% rename from Bloxstrap/NativeMethods.cs rename to Bloxstrap/Utility/NativeMethods.cs index 7cf6981..f27c0e1 100644 --- a/Bloxstrap/NativeMethods.cs +++ b/Bloxstrap/Utility/NativeMethods.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; - -namespace Bloxstrap + +namespace Bloxstrap.Utility { static class NativeMethods { diff --git a/Bloxstrap/Tools/SystemEvent.cs b/Bloxstrap/Utility/SystemEvent.cs similarity index 96% rename from Bloxstrap/Tools/SystemEvent.cs rename to Bloxstrap/Utility/SystemEvent.cs index 098dc63..05eb590 100644 --- a/Bloxstrap/Tools/SystemEvent.cs +++ b/Bloxstrap/Utility/SystemEvent.cs @@ -1,47 +1,47 @@ -/* - * Roblox Studio Mod Manager (ProjectSrc/Utility/SystemEvent.cs) - * MIT License - * Copyright (c) 2015-present MaximumADHD -*/ - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Bloxstrap.Tools -{ - public class SystemEvent : EventWaitHandle - { - public string Name { get; private set; } - - public SystemEvent(string name, bool init = false, EventResetMode mode = EventResetMode.AutoReset) : base(init, mode, name) - { - if (init) - Reset(); - else - Set(); - - Name = name; - } - - public override string ToString() - { - return Name; - } - - public Task WaitForEvent() - { - return Task.Run(WaitOne); - } - - public Task WaitForEvent(TimeSpan timeout, bool exitContext = false) - { - return Task.Run(() => WaitOne(timeout, exitContext)); - } - - public Task WaitForEvent(int millisecondsTimeout, bool exitContext = false) - { - return Task.Run(() => WaitOne(millisecondsTimeout, exitContext)); - } - } -} +/* + * Roblox Studio Mod Manager (ProjectSrc/Utility/SystemEvent.cs) + * MIT License + * Copyright (c) 2015-present MaximumADHD +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Bloxstrap.Tools +{ + public class SystemEvent : EventWaitHandle + { + public string Name { get; private set; } + + public SystemEvent(string name, bool init = false, EventResetMode mode = EventResetMode.AutoReset) : base(init, mode, name) + { + if (init) + Reset(); + else + Set(); + + Name = name; + } + + public override string ToString() + { + return Name; + } + + public Task WaitForEvent() + { + return Task.Run(WaitOne); + } + + public Task WaitForEvent(TimeSpan timeout, bool exitContext = false) + { + return Task.Run(() => WaitOne(timeout, exitContext)); + } + + public Task WaitForEvent(int millisecondsTimeout, bool exitContext = false) + { + return Task.Run(() => WaitOne(millisecondsTimeout, exitContext)); + } + } +} diff --git a/Bloxstrap/WindowScaling.cs b/Bloxstrap/Utility/WindowScaling.cs similarity index 78% rename from Bloxstrap/WindowScaling.cs rename to Bloxstrap/Utility/WindowScaling.cs index 3908587..c75139e 100644 --- a/Bloxstrap/WindowScaling.cs +++ b/Bloxstrap/Utility/WindowScaling.cs @@ -1,34 +1,31 @@ -using System; -using System.Windows; -using System.Windows.Forms; - -namespace Bloxstrap -{ - public static class WindowScaling - { - public static double GetFactor() - { - return Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth; - } - - public static int GetScaledNumber(int number) - { - return (int)Math.Ceiling(number * GetFactor()); - } - - public static System.Drawing.Size GetScaledSize(System.Drawing.Size size) - { - return new System.Drawing.Size(GetScaledNumber(size.Width), GetScaledNumber(size.Height)); - } - - public static System.Drawing.Point GetScaledPoint(System.Drawing.Point point) - { - return new System.Drawing.Point(GetScaledNumber(point.X), GetScaledNumber(point.Y)); - } - - public static Padding GetScaledPadding(Padding padding) - { - return new Padding(GetScaledNumber(padding.Left), GetScaledNumber(padding.Top), GetScaledNumber(padding.Right), GetScaledNumber(padding.Bottom)); - } - } -} +using System; +using System.Windows; +using System.Windows.Forms; + +namespace Bloxstrap.Utility +{ + public static class WindowScaling + { + public static double ScaleFactor => Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth; + + public static int GetScaledNumber(int number) + { + return (int)Math.Ceiling(number * ScaleFactor); + } + + public static System.Drawing.Size GetScaledSize(System.Drawing.Size size) + { + return new System.Drawing.Size(GetScaledNumber(size.Width), GetScaledNumber(size.Height)); + } + + public static System.Drawing.Point GetScaledPoint(System.Drawing.Point point) + { + return new System.Drawing.Point(GetScaledNumber(point.X), GetScaledNumber(point.Y)); + } + + public static Padding GetScaledPadding(Padding padding) + { + return new Padding(GetScaledNumber(padding.Left), GetScaledNumber(padding.Top), GetScaledNumber(padding.Right), GetScaledNumber(padding.Bottom)); + } + } +} From 6af7188ffefd894f350b04fa28b635ea3c5987e6 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 22:57:09 +0100 Subject: [PATCH 064/111] Add notice for missing Windows Media components --- Bloxstrap/Bootstrapper.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 66cfddc..5c0c9df 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -261,6 +261,14 @@ namespace Bloxstrap return; } + if (!File.Exists("C:\\Windows\\System32\\mfplat.dll")) + { + App.ShowMessageBox("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.", MessageBoxImage.Error); + 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; + } + _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) From 17c36ccb91a90d45af27f8269f63d87447f113bb Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 26 Jun 2023 23:10:39 +0100 Subject: [PATCH 065/111] Line ending normalization... again... --- Bloxstrap/App.xaml.cs | 710 +++++------ Bloxstrap/Bootstrapper.cs | 2454 ++++++++++++++++++------------------- 2 files changed, 1582 insertions(+), 1582 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index affaefa..d2c1cd0 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -1,357 +1,357 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Threading; - -using Microsoft.Win32; - -using Bloxstrap.Extensions; -using Bloxstrap.Models; -using Bloxstrap.UI.BootstrapperDialogs; -using Bloxstrap.UI.Menu.Views; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; + +using Microsoft.Win32; + +using Bloxstrap.Extensions; +using Bloxstrap.Models; +using Bloxstrap.UI.BootstrapperDialogs; +using Bloxstrap.UI.Menu.Views; using Bloxstrap.Utility; -namespace Bloxstrap -{ - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - public static readonly CultureInfo CultureFormat = CultureInfo.InvariantCulture; - - public const string ProjectName = "Bloxstrap"; - public const string ProjectRepository = "pizzaboxer/bloxstrap"; - public const string RobloxAppName = "RobloxPlayerBeta"; - - // used only for communicating between app and menu - use Directories.Base for anything else - public static string BaseDirectory = null!; - public static bool ShouldSaveConfigs { get; set; } = false; - public static bool IsSetupComplete { get; set; } = true; - public static bool IsFirstRun { get; private set; } = true; - public static bool IsQuiet { get; private set; } = false; - public static bool IsUninstall { get; private set; } = false; - public static bool IsNoLaunch { get; private set; } = false; - public static bool IsUpgrade { get; private set; } = false; - public static bool IsMenuLaunch { get; private set; } = false; - public static string[] LaunchArgs { get; private set; } = null!; - - public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; - - // singletons - 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(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }); - - public static System.Windows.Forms.NotifyIcon Notification { get; private set; } = null!; - - // shorthand - public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK) - { - if (IsQuiet) - return MessageBoxResult.None; - - return MessageBox.Show(message, ProjectName, buttons, icon); - } - - public static void Terminate(int code = Bootstrapper.ERROR_SUCCESS) - { - Logger.WriteLine($"[App::Terminate] Terminating with exit code {code}"); - Settings.Save(); - State.Save(); - Notification.Dispose(); - Environment.Exit(code); - } - - private void InitLog() - { - // if we're running for the first time or uninstalling, log to temp folder - // else, log to bloxstrap folder - - bool isUsingTempDir = IsFirstRun || IsUninstall; - string logdir = isUsingTempDir ? Path.Combine(Directories.LocalAppData, "Temp") : Path.Combine(Directories.Base, "Logs"); - string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); - int processId = Process.GetCurrentProcess().Id; - - Logger.Initialize(Path.Combine(logdir, $"{ProjectName}_{timestamp}_{processId}.log")); - - // clean up any logs older than a week - if (!isUsingTempDir) - { - foreach (FileInfo log in new DirectoryInfo(logdir).GetFiles()) - { - if (log.LastWriteTimeUtc.AddDays(7) > DateTime.UtcNow) - continue; - - Logger.WriteLine($"[App::InitLog] Cleaning up old log file '{log.Name}'"); - log.Delete(); - } - } - } - - void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e) - { - e.Handled = true; - - Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread"); - Logger.WriteLine($"[App::OnStartup] {e.Exception}"); - - if (!IsQuiet) - Settings.Prop.BootstrapperStyle.GetNew().ShowError($"{e.Exception.GetType()}: {e.Exception.Message}"); - - Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); - } - - protected override void OnStartup(StartupEventArgs e) - { - base.OnStartup(e); - - Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}"); - - // To customize application configuration such as set high DPI settings or default font, - // see https://aka.ms/applicationconfiguration. - ApplicationConfiguration.Initialize(); - - LaunchArgs = e.Args; - - HttpClient.Timeout = TimeSpan.FromMinutes(5); - HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); - - if (LaunchArgs.Length > 0) - { - if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1) - { - Logger.WriteLine("[App::OnStartup] Started with IsMenuLaunch flag"); - IsMenuLaunch = true; - } - - if (Array.IndexOf(LaunchArgs, "-quiet") != -1) - { - Logger.WriteLine("[App::OnStartup] Started with IsQuiet flag"); - IsQuiet = true; - } - - if (Array.IndexOf(LaunchArgs, "-uninstall") != -1) - { - Logger.WriteLine("[App::OnStartup] Started with IsUninstall flag"); - IsUninstall = true; - } - - if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1) - { - Logger.WriteLine("[App::OnStartup] Started with IsNoLaunch flag"); - IsNoLaunch = true; - } - - if (Array.IndexOf(LaunchArgs, "-upgrade") != -1) - { - Logger.WriteLine("[App::OnStartup] Bloxstrap started with IsUpgrade flag"); - IsUpgrade = true; - } - } - - // so this needs to be here because winforms moment - // onclick events will not fire unless this is defined here in the main thread so uhhhhh - // we'll show the icon if we're launching roblox since we're likely gonna be showing a - // bunch of notifications, and always showing it just makes the most sense i guess since it - // indicates that bloxstrap is running, even in the background - Notification = new() - { - Icon = Bloxstrap.Properties.Resources.IconBloxstrap, - Text = ProjectName, - Visible = !IsMenuLaunch - }; - - // check if installed - using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}")) - { - string? installLocation = null; - - if (registryKey is not null) - installLocation = (string?)registryKey.GetValue("InstallLocation"); - - if (registryKey is null || installLocation is null) - { - Logger.WriteLine("[App::OnStartup] Running first-time install"); - - BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName); - InitLog(); - - if (!IsQuiet) - { - IsSetupComplete = false; - FastFlags.Load(); - new MainWindow().ShowDialog(); - } - } - else - { - IsFirstRun = false; - BaseDirectory = installLocation; - } - } - - // exit if we don't click the install button on installation - if (!IsSetupComplete) - { - Logger.WriteLine("[App::OnStartup] Installation cancelled!"); - Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT); - } - - Directories.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) - { - InitLog(); - Settings.Load(); - State.Load(); - FastFlags.Load(); - } - -#if !DEBUG - if (!IsUninstall && !IsFirstRun) - Updater.CheckInstalledVersion(); -#endif - - string commandLine = ""; - - if (IsMenuLaunch) - { - Process? menuProcess = Process.GetProcesses().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault(); - - if (menuProcess is not null) - { - IntPtr handle = menuProcess.MainWindowHandle; - Logger.WriteLine($"[App::OnStartup] Found an already existing menu window with handle {handle}"); - NativeMethods.SetForegroundWindow(handle); - } - else - { - if (Process.GetProcessesByName(ProjectName).Length > 1) - ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); - - new MainWindow().ShowDialog(); - } - } - else if (LaunchArgs.Length > 0) - { - if (LaunchArgs[0].StartsWith("roblox-player:")) - { - commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); - } - else if (LaunchArgs[0].StartsWith("roblox:")) - { - if (Settings.Prop.UseDisableAppPatch) - ShowMessageBox("Roblox was launched via a deeplink, however the desktop app is required for deeplink launching to work. Because you've opted to disable the desktop app, it will temporarily be re-enabled for this launch only.", MessageBoxImage.Information); - - commandLine = $"--app --deeplink {LaunchArgs[0]}"; - } - else - { - commandLine = "--app"; - } - } - else - { - commandLine = "--app"; - } - - if (!String.IsNullOrEmpty(commandLine)) - { - if (!IsFirstRun) - ShouldSaveConfigs = true; - - // start bootstrapper and show the bootstrapper modal if we're not running silently - Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper"); - Bootstrapper bootstrapper = new(commandLine); - IBootstrapperDialog? dialog = null; - - if (!IsQuiet) - { - Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog"); - dialog = Settings.Prop.BootstrapperStyle.GetNew(); - bootstrapper.Dialog = dialog; - dialog.Bootstrapper = bootstrapper; - } - - // handle roblox singleton mutex for multi-instance launching - // note we're handling it here in the main thread and NOT in the - // bootstrapper as handling mutexes in async contexts suuuuuucks - - Mutex? singletonMutex = null; - - if (Settings.Prop.MultiInstanceLaunching) - { - Logger.WriteLine("[App::OnStartup] Creating singleton mutex"); - - try - { - Mutex.OpenExisting("ROBLOX_singletonMutex"); - Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!"); - } - catch - { - // create the singleton mutex before the game client does - singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); - } - } - - // there's a bug here that i have yet to fix! - // sometimes the task just terminates when the bootstrapper hasn't - // actually finished, causing the bootstrapper to hang indefinitely - // i have no idea how the fuck this happens, but it happens like VERY - // rarely so i'm not too concerned by it - // maybe one day ill find out why it happens - Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => - { - Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished"); - - if (t.IsFaulted) - Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper"); - - if (t.Exception is null) - return; - - Logger.WriteLine($"[App::OnStartup] {t.Exception}"); - -#if DEBUG - throw t.Exception; -#else - var exception = t.Exception.InnerExceptions.Count >= 1 ? t.Exception.InnerExceptions[0] : t.Exception; - dialog?.ShowError($"{exception.GetType()}: {exception.Message}"); - Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); -#endif - }); - - dialog?.ShowBootstrapper(); - bootstrapperTask.Wait(); - - if (singletonMutex is not null) - { - Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed"); - - // we've got ownership of the roblox singleton mutex! - // if we stop running, everything will screw up once any more roblox instances launched - while (Process.GetProcessesByName("RobloxPlayerBeta").Any()) - Thread.Sleep(5000); - } - } - - Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating..."); - - Terminate(); - } - } -} +namespace Bloxstrap +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + public static readonly CultureInfo CultureFormat = CultureInfo.InvariantCulture; + + public const string ProjectName = "Bloxstrap"; + public const string ProjectRepository = "pizzaboxer/bloxstrap"; + public const string RobloxAppName = "RobloxPlayerBeta"; + + // used only for communicating between app and menu - use Directories.Base for anything else + public static string BaseDirectory = null!; + public static bool ShouldSaveConfigs { get; set; } = false; + public static bool IsSetupComplete { get; set; } = true; + public static bool IsFirstRun { get; private set; } = true; + public static bool IsQuiet { get; private set; } = false; + public static bool IsUninstall { get; private set; } = false; + public static bool IsNoLaunch { get; private set; } = false; + public static bool IsUpgrade { get; private set; } = false; + public static bool IsMenuLaunch { get; private set; } = false; + public static string[] LaunchArgs { get; private set; } = null!; + + public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; + + // singletons + 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(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }); + + public static System.Windows.Forms.NotifyIcon Notification { get; private set; } = null!; + + // shorthand + public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK) + { + if (IsQuiet) + return MessageBoxResult.None; + + return MessageBox.Show(message, ProjectName, buttons, icon); + } + + public static void Terminate(int code = Bootstrapper.ERROR_SUCCESS) + { + Logger.WriteLine($"[App::Terminate] Terminating with exit code {code}"); + Settings.Save(); + State.Save(); + Notification.Dispose(); + Environment.Exit(code); + } + + private void InitLog() + { + // if we're running for the first time or uninstalling, log to temp folder + // else, log to bloxstrap folder + + bool isUsingTempDir = IsFirstRun || IsUninstall; + string logdir = isUsingTempDir ? Path.Combine(Directories.LocalAppData, "Temp") : Path.Combine(Directories.Base, "Logs"); + string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); + int processId = Process.GetCurrentProcess().Id; + + Logger.Initialize(Path.Combine(logdir, $"{ProjectName}_{timestamp}_{processId}.log")); + + // clean up any logs older than a week + if (!isUsingTempDir) + { + foreach (FileInfo log in new DirectoryInfo(logdir).GetFiles()) + { + if (log.LastWriteTimeUtc.AddDays(7) > DateTime.UtcNow) + continue; + + Logger.WriteLine($"[App::InitLog] Cleaning up old log file '{log.Name}'"); + log.Delete(); + } + } + } + + void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + + Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread"); + Logger.WriteLine($"[App::OnStartup] {e.Exception}"); + + if (!IsQuiet) + Settings.Prop.BootstrapperStyle.GetNew().ShowError($"{e.Exception.GetType()}: {e.Exception.Message}"); + + Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); + } + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}"); + + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + LaunchArgs = e.Args; + + HttpClient.Timeout = TimeSpan.FromMinutes(5); + HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); + + if (LaunchArgs.Length > 0) + { + if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1) + { + Logger.WriteLine("[App::OnStartup] Started with IsMenuLaunch flag"); + IsMenuLaunch = true; + } + + if (Array.IndexOf(LaunchArgs, "-quiet") != -1) + { + Logger.WriteLine("[App::OnStartup] Started with IsQuiet flag"); + IsQuiet = true; + } + + if (Array.IndexOf(LaunchArgs, "-uninstall") != -1) + { + Logger.WriteLine("[App::OnStartup] Started with IsUninstall flag"); + IsUninstall = true; + } + + if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1) + { + Logger.WriteLine("[App::OnStartup] Started with IsNoLaunch flag"); + IsNoLaunch = true; + } + + if (Array.IndexOf(LaunchArgs, "-upgrade") != -1) + { + Logger.WriteLine("[App::OnStartup] Bloxstrap started with IsUpgrade flag"); + IsUpgrade = true; + } + } + + // so this needs to be here because winforms moment + // onclick events will not fire unless this is defined here in the main thread so uhhhhh + // we'll show the icon if we're launching roblox since we're likely gonna be showing a + // bunch of notifications, and always showing it just makes the most sense i guess since it + // indicates that bloxstrap is running, even in the background + Notification = new() + { + Icon = Bloxstrap.Properties.Resources.IconBloxstrap, + Text = ProjectName, + Visible = !IsMenuLaunch + }; + + // check if installed + using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}")) + { + string? installLocation = null; + + if (registryKey is not null) + installLocation = (string?)registryKey.GetValue("InstallLocation"); + + if (registryKey is null || installLocation is null) + { + Logger.WriteLine("[App::OnStartup] Running first-time install"); + + BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName); + InitLog(); + + if (!IsQuiet) + { + IsSetupComplete = false; + FastFlags.Load(); + new MainWindow().ShowDialog(); + } + } + else + { + IsFirstRun = false; + BaseDirectory = installLocation; + } + } + + // exit if we don't click the install button on installation + if (!IsSetupComplete) + { + Logger.WriteLine("[App::OnStartup] Installation cancelled!"); + Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT); + } + + Directories.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) + { + InitLog(); + Settings.Load(); + State.Load(); + FastFlags.Load(); + } + +#if !DEBUG + if (!IsUninstall && !IsFirstRun) + Updater.CheckInstalledVersion(); +#endif + + string commandLine = ""; + + if (IsMenuLaunch) + { + Process? menuProcess = Process.GetProcesses().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault(); + + if (menuProcess is not null) + { + IntPtr handle = menuProcess.MainWindowHandle; + Logger.WriteLine($"[App::OnStartup] Found an already existing menu window with handle {handle}"); + NativeMethods.SetForegroundWindow(handle); + } + else + { + if (Process.GetProcessesByName(ProjectName).Length > 1) + ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); + + new MainWindow().ShowDialog(); + } + } + else if (LaunchArgs.Length > 0) + { + if (LaunchArgs[0].StartsWith("roblox-player:")) + { + commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); + } + else if (LaunchArgs[0].StartsWith("roblox:")) + { + if (Settings.Prop.UseDisableAppPatch) + ShowMessageBox("Roblox was launched via a deeplink, however the desktop app is required for deeplink launching to work. Because you've opted to disable the desktop app, it will temporarily be re-enabled for this launch only.", MessageBoxImage.Information); + + commandLine = $"--app --deeplink {LaunchArgs[0]}"; + } + else + { + commandLine = "--app"; + } + } + else + { + commandLine = "--app"; + } + + if (!String.IsNullOrEmpty(commandLine)) + { + if (!IsFirstRun) + ShouldSaveConfigs = true; + + // start bootstrapper and show the bootstrapper modal if we're not running silently + Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper"); + Bootstrapper bootstrapper = new(commandLine); + IBootstrapperDialog? dialog = null; + + if (!IsQuiet) + { + Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog"); + dialog = Settings.Prop.BootstrapperStyle.GetNew(); + bootstrapper.Dialog = dialog; + dialog.Bootstrapper = bootstrapper; + } + + // handle roblox singleton mutex for multi-instance launching + // note we're handling it here in the main thread and NOT in the + // bootstrapper as handling mutexes in async contexts suuuuuucks + + Mutex? singletonMutex = null; + + if (Settings.Prop.MultiInstanceLaunching) + { + Logger.WriteLine("[App::OnStartup] Creating singleton mutex"); + + try + { + Mutex.OpenExisting("ROBLOX_singletonMutex"); + Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!"); + } + catch + { + // create the singleton mutex before the game client does + singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); + } + } + + // there's a bug here that i have yet to fix! + // sometimes the task just terminates when the bootstrapper hasn't + // actually finished, causing the bootstrapper to hang indefinitely + // i have no idea how the fuck this happens, but it happens like VERY + // rarely so i'm not too concerned by it + // maybe one day ill find out why it happens + Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => + { + Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished"); + + if (t.IsFaulted) + Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper"); + + if (t.Exception is null) + return; + + Logger.WriteLine($"[App::OnStartup] {t.Exception}"); + +#if DEBUG + throw t.Exception; +#else + var exception = t.Exception.InnerExceptions.Count >= 1 ? t.Exception.InnerExceptions[0] : t.Exception; + dialog?.ShowError($"{exception.GetType()}: {exception.Message}"); + Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); +#endif + }); + + dialog?.ShowBootstrapper(); + bootstrapperTask.Wait(); + + if (singletonMutex is not null) + { + Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed"); + + // we've got ownership of the roblox singleton mutex! + // if we stop running, everything will screw up once any more roblox instances launched + while (Process.GetProcessesByName("RobloxPlayerBeta").Any()) + Thread.Sleep(5000); + } + } + + Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating..."); + + Terminate(); + } + } +} diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 5c0c9df..5c17080 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1,1233 +1,1233 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using System.Windows; - -using Microsoft.Win32; - -using Bloxstrap.Enums; -using Bloxstrap.Extensions; -using Bloxstrap.Integrations; -using Bloxstrap.Models; -using Bloxstrap.Tools; -using Bloxstrap.UI.BootstrapperDialogs; - -namespace Bloxstrap -{ - public class Bootstrapper - { - #region Properties - - // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes - public const int ERROR_SUCCESS = 0; - public const int ERROR_INSTALL_USEREXIT = 1602; - public const int ERROR_INSTALL_FAILURE = 1603; - - // in case a new package is added, you can find the corresponding directory - // by opening the stock bootstrapper in a hex editor - // TODO - there ideally should be a less static way to do this that's not hardcoded? - private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() - { - { "RobloxApp.zip", @"" }, - { "shaders.zip", @"shaders\" }, - { "ssl.zip", @"ssl\" }, - - // the runtime installer is only extracted if it needs installing - { "WebView2.zip", @"" }, - { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, - - { "content-avatar.zip", @"content\avatar\" }, - { "content-configs.zip", @"content\configs\" }, - { "content-fonts.zip", @"content\fonts\" }, - { "content-sky.zip", @"content\sky\" }, - { "content-sounds.zip", @"content\sounds\" }, - { "content-textures2.zip", @"content\textures\" }, - { "content-models.zip", @"content\models\" }, - - { "content-textures3.zip", @"PlatformContent\pc\textures\" }, - { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, - { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, - - { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, - { "extracontent-translations.zip", @"ExtraContent\translations\" }, - { "extracontent-models.zip", @"ExtraContent\models\" }, - { "extracontent-textures.zip", @"ExtraContent\textures\" }, - { "extracontent-places.zip", @"ExtraContent\places\" }, - }; - - private const string AppSettings = - "\r\n" + - "\r\n" + - " content\r\n" + - " http://www.roblox.com\r\n" + - "\r\n"; - - private readonly CancellationTokenSource _cancelTokenSource = new(); - - private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); - private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); - - private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); - - private string _launchCommandLine; - - private string _latestVersionGuid = null!; - private PackageManifest _versionPackageManifest = null!; - private string _versionFolder = null!; - - private bool _isInstalling = false; - private double _progressIncrement; - private long _totalDownloadedBytes = 0; - private int _packagesExtracted = 0; - private bool _cancelFired = false; - - public IBootstrapperDialog? Dialog = null; - #endregion - - #region Core - public Bootstrapper(string launchCommandLine) - { - _launchCommandLine = launchCommandLine; - } - - private void SetStatus(string message) - { - App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); - - // yea idk - if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) - message = message.Replace("...", ""); - - if (Dialog is not null) - Dialog.Message = message; - } - - private void UpdateProgressbar() - { - int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); - - // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 - // too lazy to fix properly so lol - if (newProgress > 100) - return; - - if (Dialog is not null) - Dialog.ProgressValue = newProgress; - } - - public async Task Run() - { - App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); - - if (App.IsUninstall) - { - Uninstall(); - return; - } - -#if !DEBUG - if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) - await CheckForUpdates(); -#endif - - // ensure only one instance of the bootstrapper is running at the time - // so that we don't have stuff like two updates happening simultaneously - - bool mutexExists = false; - - try - { - Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); - App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); - mutexExists = true; - } - catch (Exception) - { - // no mutex exists - } - - // wait for mutex to be released if it's not yet - await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex"); - await mutex.AcquireAsync(_cancelTokenSource.Token); - - // reload our configs since they've likely changed by now - if (mutexExists) - { - App.Settings.Load(); - App.State.Load(); - } - - await CheckLatestVersion(); - - CheckInstallMigration(); - - // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) - await InstallLatestVersion(); - - if (App.IsFirstRun) - App.ShouldSaveConfigs = true; - - MigrateIntegrations(); - - await InstallWebView2(); - - App.FastFlags.Save(); - await ApplyModifications(); - - if (App.IsFirstRun || FreshInstall) - { - Register(); - 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.IsNoLaunch) - Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed"); - else if (!App.IsNoLaunch && !_cancelFired) - await StartRoblox(); - } - - private async Task CheckLatestVersion() - { - SetStatus("Connecting to Roblox..."); - - ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); - - // briefly check if current channel is suitable to use - if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) - { - string? switchDefaultPrompt = null; - ClientVersion? defaultChannelInfo = null; - - App.Logger.WriteLine($"[Bootstrapper::CheckLatestVersion] Checking if current channel is suitable to use..."); - - if (String.IsNullOrEmpty(switchDefaultPrompt)) - { - // this SUCKS - defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - int defaultChannelVersion = Int32.Parse(defaultChannelInfo.Version.Split('.')[1]); - int currentChannelVersion = Int32.Parse(clientVersion.Version.Split('.')[1]); - - if (currentChannelVersion < defaultChannelVersion) - switchDefaultPrompt = $"Your current preferred channel ({App.Settings.Prop.Channel}) appears to no longer be receiving updates. Would you like to switch to {RobloxDeployment.DefaultChannel}?"; - } - - if (!String.IsNullOrEmpty(switchDefaultPrompt)) - { - MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(switchDefaultPrompt, MessageBoxImage.Question, MessageBoxButton.YesNo); - - if (result == MessageBoxResult.Yes) - { - App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; - App.Logger.WriteLine($"[DeployManager::SwitchToDefault] Changed Roblox release channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); - - if (defaultChannelInfo is null) - defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - - clientVersion = defaultChannelInfo; - } - } - } - - _latestVersionGuid = clientVersion.VersionGuid; - _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); - _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); - } - - private async Task StartRoblox() - { - SetStatus("Starting Roblox..."); - - if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) - { - Utilities.ShellExecute("https://www.roblox.com/games"); - Dialog?.CloseBootstrapper(); - return; - } - +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Windows; + +using Microsoft.Win32; + +using Bloxstrap.Enums; +using Bloxstrap.Extensions; +using Bloxstrap.Integrations; +using Bloxstrap.Models; +using Bloxstrap.Tools; +using Bloxstrap.UI.BootstrapperDialogs; + +namespace Bloxstrap +{ + public class Bootstrapper + { + #region Properties + + // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes + public const int ERROR_SUCCESS = 0; + public const int ERROR_INSTALL_USEREXIT = 1602; + public const int ERROR_INSTALL_FAILURE = 1603; + + // in case a new package is added, you can find the corresponding directory + // by opening the stock bootstrapper in a hex editor + // TODO - there ideally should be a less static way to do this that's not hardcoded? + private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() + { + { "RobloxApp.zip", @"" }, + { "shaders.zip", @"shaders\" }, + { "ssl.zip", @"ssl\" }, + + // the runtime installer is only extracted if it needs installing + { "WebView2.zip", @"" }, + { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, + + { "content-avatar.zip", @"content\avatar\" }, + { "content-configs.zip", @"content\configs\" }, + { "content-fonts.zip", @"content\fonts\" }, + { "content-sky.zip", @"content\sky\" }, + { "content-sounds.zip", @"content\sounds\" }, + { "content-textures2.zip", @"content\textures\" }, + { "content-models.zip", @"content\models\" }, + + { "content-textures3.zip", @"PlatformContent\pc\textures\" }, + { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, + { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, + + { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, + { "extracontent-translations.zip", @"ExtraContent\translations\" }, + { "extracontent-models.zip", @"ExtraContent\models\" }, + { "extracontent-textures.zip", @"ExtraContent\textures\" }, + { "extracontent-places.zip", @"ExtraContent\places\" }, + }; + + private const string AppSettings = + "\r\n" + + "\r\n" + + " content\r\n" + + " http://www.roblox.com\r\n" + + "\r\n"; + + private readonly CancellationTokenSource _cancelTokenSource = new(); + + private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); + private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); + + private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); + + private string _launchCommandLine; + + private string _latestVersionGuid = null!; + private PackageManifest _versionPackageManifest = null!; + private string _versionFolder = null!; + + private bool _isInstalling = false; + private double _progressIncrement; + private long _totalDownloadedBytes = 0; + private int _packagesExtracted = 0; + private bool _cancelFired = false; + + public IBootstrapperDialog? Dialog = null; + #endregion + + #region Core + public Bootstrapper(string launchCommandLine) + { + _launchCommandLine = launchCommandLine; + } + + private void SetStatus(string message) + { + App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); + + // yea idk + if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) + message = message.Replace("...", ""); + + if (Dialog is not null) + Dialog.Message = message; + } + + private void UpdateProgressbar() + { + int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); + + // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 + // too lazy to fix properly so lol + if (newProgress > 100) + return; + + if (Dialog is not null) + Dialog.ProgressValue = newProgress; + } + + public async Task Run() + { + App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); + + if (App.IsUninstall) + { + Uninstall(); + return; + } + +#if !DEBUG + if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) + await CheckForUpdates(); +#endif + + // ensure only one instance of the bootstrapper is running at the time + // so that we don't have stuff like two updates happening simultaneously + + bool mutexExists = false; + + try + { + Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); + App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); + mutexExists = true; + } + catch (Exception) + { + // no mutex exists + } + + // wait for mutex to be released if it's not yet + await using AsyncMutex mutex = new("Bloxstrap_BootstrapperMutex"); + await mutex.AcquireAsync(_cancelTokenSource.Token); + + // reload our configs since they've likely changed by now + if (mutexExists) + { + App.Settings.Load(); + App.State.Load(); + } + + await CheckLatestVersion(); + + CheckInstallMigration(); + + // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist + if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) + await InstallLatestVersion(); + + if (App.IsFirstRun) + App.ShouldSaveConfigs = true; + + MigrateIntegrations(); + + await InstallWebView2(); + + App.FastFlags.Save(); + await ApplyModifications(); + + if (App.IsFirstRun || FreshInstall) + { + Register(); + 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.IsNoLaunch) + Dialog?.ShowSuccess($"{App.ProjectName} has successfully installed"); + else if (!App.IsNoLaunch && !_cancelFired) + await StartRoblox(); + } + + private async Task CheckLatestVersion() + { + SetStatus("Connecting to Roblox..."); + + ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + + // briefly check if current channel is suitable to use + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) + { + string? switchDefaultPrompt = null; + ClientVersion? defaultChannelInfo = null; + + App.Logger.WriteLine($"[Bootstrapper::CheckLatestVersion] Checking if current channel is suitable to use..."); + + if (String.IsNullOrEmpty(switchDefaultPrompt)) + { + // this SUCKS + defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + int defaultChannelVersion = Int32.Parse(defaultChannelInfo.Version.Split('.')[1]); + int currentChannelVersion = Int32.Parse(clientVersion.Version.Split('.')[1]); + + if (currentChannelVersion < defaultChannelVersion) + switchDefaultPrompt = $"Your current preferred channel ({App.Settings.Prop.Channel}) appears to no longer be receiving updates. Would you like to switch to {RobloxDeployment.DefaultChannel}?"; + } + + if (!String.IsNullOrEmpty(switchDefaultPrompt)) + { + MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(switchDefaultPrompt, MessageBoxImage.Question, MessageBoxButton.YesNo); + + if (result == MessageBoxResult.Yes) + { + App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; + App.Logger.WriteLine($"[DeployManager::SwitchToDefault] Changed Roblox release channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); + + if (defaultChannelInfo is null) + defaultChannelInfo = await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + + clientVersion = defaultChannelInfo; + } + } + } + + _latestVersionGuid = clientVersion.VersionGuid; + _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); + _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); + } + + private async Task StartRoblox() + { + SetStatus("Starting Roblox..."); + + if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) + { + Utilities.ShellExecute("https://www.roblox.com/games"); + Dialog?.CloseBootstrapper(); + return; + } + if (!File.Exists("C:\\Windows\\System32\\mfplat.dll")) { App.ShowMessageBox("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.", MessageBoxImage.Error); 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; - } - - _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); - - if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) - _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); - - // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes - bool shouldWait = false; - - // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now - int gameClientPid; - using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) - { - gameClientPid = gameClient.Id; - } - - List autocloseProcesses = new(); - RobloxActivity? activityWatcher = null; - DiscordRichPresence? richPresence = null; - ServerNotifier? serverNotifier = null; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); - - using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) - { - bool startEventFired = await startEvent.WaitForEvent(); - - startEvent.Close(); - - if (!startEventFired) - return; - } - - if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) - { - activityWatcher = new(); - shouldWait = true; - } - - if (App.Settings.Prop.UseDiscordRichPresence) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); - richPresence = new(activityWatcher!); - } - - if (App.Settings.Prop.ShowServerDetails) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); - serverNotifier = new(activityWatcher!); - } - - // launch custom integrations now - foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); - - try - { - Process process = Process.Start(integration.Location, integration.LaunchArgs); - - if (integration.AutoClose) - { - shouldWait = true; - autocloseProcesses.Add(process); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); - } - } - - // event fired, wait for 3 seconds then close - await Task.Delay(3000); - Dialog?.CloseBootstrapper(); - - // keep bloxstrap open in the background if needed - if (!shouldWait) - return; - - activityWatcher?.StartWatcher(); - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); - - while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) - await Task.Delay(1000); - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); - - richPresence?.Dispose(); - - foreach (Process process in autocloseProcesses) - { - if (process.HasExited) - continue; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); - process.Kill(); - } - } - - public void CancelInstall() - { - if (!_isInstalling) - { - App.Terminate(ERROR_INSTALL_USEREXIT); - return; - } - - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); - - _cancelTokenSource.Cancel(); - _cancelFired = true; - - try - { - // clean up install - if (App.IsFirstRun) - Directory.Delete(Directories.Base, true); - else if (Directory.Exists(_versionFolder)) - Directory.Delete(_versionFolder, true); - } - catch (Exception ex) - { - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); - App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); - } - - App.Terminate(ERROR_INSTALL_USEREXIT); - } - #endregion - - #region App Install - public static void Register() - { - using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) - { - applicationKey.SetValue("InstallLocation", Directories.Base); - } - - // set uninstall key - using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) - { - uninstallKey.SetValue("DisplayIcon", $"{Directories.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", Directories.Base); - uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", "pizzaboxer"); - uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); - uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); - uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); - uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); - uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); - } - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); - } - - public void RegisterProgramSize() - { - App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); - - using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); - - // sum compressed and uncompressed package sizes and convert to kilobytes - int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; - - uninstallKey.SetValue("EstimatedSize", totalSize); - - App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); - } - - private void CheckInstallMigration() - { - // check if we've changed our install location since the last time we started - // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff - - using RegistryKey? applicationKey = Registry.CurrentUser.OpenSubKey($@"Software\{App.ProjectName}", true); - - string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); - - if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) - return; - - SetStatus("Migrating install location..."); - - if (Directory.Exists(oldInstallLocation)) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); - - foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) - { - string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); - string newFileLocation = Path.Combine(Directories.Base, relativeFile); - string? newDirectory = Path.GetDirectoryName(newFileLocation); - - try - { - if (!String.IsNullOrEmpty(newDirectory)) - Directory.CreateDirectory(newDirectory); - - File.Move(oldFileLocation, newFileLocation, true); - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); - } - } - - try - { - Directory.Delete(oldInstallLocation, true); - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); - } - } - - applicationKey.DeleteValue("OldInstallLocation"); - - // allow shortcuts to be re-registered - if (Directory.Exists(Directories.StartMenu)) - Directory.Delete(Directories.StartMenu, true); - - if (File.Exists(DesktopShortcutLocation)) - { - File.Delete(DesktopShortcutLocation); - App.Settings.Prop.CreateDesktopIcon = true; - } - - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); - } - - public static void CheckInstall() - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); - - // check if launch uri is set to our bootstrapper - // this doesn't go under register, so we check every launch - // just in case the stock bootstrapper changes it back - - ProtocolHandler.Register("roblox", "Roblox", Directories.Application); - ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); - - // in case the user is reinstalling - if (File.Exists(Directories.Application) && App.IsFirstRun) - File.Delete(Directories.Application); - - // check to make sure bootstrapper is in the install folder - if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) - File.Copy(Environment.ProcessPath, Directories.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(Directories.StartMenu)) - { - Directory.CreateDirectory(Directories.StartMenu); - - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); - - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); - } - else - { - // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" - string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); - string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); - - if (File.Exists(oldMenuShortcut)) - File.Delete(oldMenuShortcut); - - if (!File.Exists(newMenuShortcut)) - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(newMenuShortcut); - } - - if (App.Settings.Prop.CreateDesktopIcon) - { - if (!File.Exists(DesktopShortcutLocation)) - { - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(DesktopShortcutLocation); - } - - // one-time toggle, set it back to false - App.Settings.Prop.CreateDesktopIcon = false; - } - } - - private async Task CheckForUpdates() - { - // don't update if there's another instance running (likely running in the background) - if (Process.GetProcessesByName(App.ProjectName).Count() > 1) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); - return; - } - - string currentVersion = $"{App.ProjectName} v{App.Version}"; - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); - - var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - - if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); - return; - } - - SetStatus($"Getting the latest {App.ProjectName}..."); - - // 64-bit is always the first option - GithubReleaseAsset asset = releaseInfo.Assets[0]; - string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); - - if (!File.Exists(downloadLocation)) - { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - - await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - } - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); - - ProcessStartInfo startInfo = new() - { - FileName = downloadLocation, - }; - - foreach (string arg in App.LaunchArgs) - startInfo.ArgumentList.Add(arg); - - App.Settings.Save(); - - Process.Start(startInfo); - - Environment.Exit(0); - } - - private void Uninstall() - { - // prompt to shutdown roblox if its currently running - if (Process.GetProcessesByName(App.RobloxAppName).Any()) - { - App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); - - Dialog?.PromptShutdown(); - - try - { - foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) - { - process.CloseMainWindow(); - process.Close(); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); - } - - App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); - } - - SetStatus($"Uninstalling {App.ProjectName}..."); - - //App.Settings.ShouldSave = false; - App.ShouldSaveConfigs = false; - - // check if stock bootstrapper is still installed - RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); - if (bootstrapperKey is null) - { - 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); - } - - try - { - // delete application key - Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"); - - // delete start menu folder - Directory.Delete(Directories.StartMenu, true); - - // delete desktop shortcut - File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); - - // delete uninstall key - Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); - - // delete installation folder - // (should delete everything except bloxstrap itself) - Directory.Delete(Directories.Base, true); - } - catch (Exception ex) - { - App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); - } - - Action? callback = null; - - if (Directory.Exists(Directories.Base)) - { - callback = () => - { - // 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" - - Process.Start(new ProcessStartInfo() - { - FileName = "cmd.exe", - Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - }; - } - - Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); - } - #endregion - - #region Roblox Install - private async Task InstallLatestVersion() - { - _isInstalling = true; - - SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); - - Directory.CreateDirectory(Directories.Base); - Directory.CreateDirectory(Directories.Downloads); - Directory.CreateDirectory(Directories.Versions); - - // package manifest states packed size and uncompressed size in exact bytes - // packed size only matters if we don't already have the package cached on disk - string[] cachedPackages = Directory.GetFiles(Directories.Downloads); - int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); - - if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) - { - App.ShowMessageBox($"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); - App.Terminate(ERROR_INSTALL_FAILURE); - return; - } - - if (Dialog is not null) - { - Dialog.CancelEnabled = true; - Dialog.ProgressStyle = ProgressBarStyle.Continuous; - } - - // compute total bytes to download - _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); - - foreach (Package package in _versionPackageManifest) - { - if (_cancelFired) - return; - - // download all the packages synchronously - await DownloadPackage(package); - - // we'll extract the runtime installer later if we need to - if (package.Name == "WebView2RuntimeInstaller.zip") - continue; - - // extract the package immediately after download asynchronously - // discard is just used to suppress the warning - Task _ = ExtractPackage(package); - } - - if (_cancelFired) - return; - - // allow progress bar to 100% before continuing (purely ux reasons lol) - await Task.Delay(1000); - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Marquee; - SetStatus("Configuring Roblox..."); - } - - // wait for all packages to finish extracting, with an exception for the webview2 runtime installer - while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) - { - await Task.Delay(100); - } - - string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); - await File.WriteAllTextAsync(appSettingsLocation, AppSettings); - - if (_cancelFired) - return; - - if (!FreshInstall) - { - // let's take this opportunity to delete any packages we don't need anymore - foreach (string filename in cachedPackages) - { - if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) - { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); - File.Delete(filename); - } - } - - string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); - - // move old compatibility flags for the old location - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); - string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); - - if (appFlags is not null) - { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); - appFlagsKey.SetValue(_playerLocation, appFlags); - appFlagsKey.DeleteValue(oldGameClientLocation); - } - } - - // delete any old version folders - // we only do this if roblox isnt running just in case an update happened - // while they were launching a second instance or something idk - if (!Process.GetProcessesByName(App.RobloxAppName).Any()) - { - foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) - { - if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) - continue; - - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); - dir.Delete(true); - } - } - } - - App.State.Prop.VersionGuid = _latestVersionGuid; - - // don't register program size until the program is registered, which will be done after this - if (!App.IsFirstRun && !FreshInstall) - RegisterProgramSize(); - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - _isInstalling = false; - } - - private async Task 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($"[Bootstrapper::InstallWebView2] Installing runtime..."); - - string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); - - if (!Directory.Exists(baseDirectory)) - { - Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); - - if (package is null) - { - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); - return; - } - - await ExtractPackage(package); - } - - SetStatus("Installing WebView2, please wait..."); - - ProcessStartInfo startInfo = new() - { - WorkingDirectory = baseDirectory, - FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), - Arguments = "/silent /install" - }; - - await Process.Start(startInfo)!.WaitForExitAsync(); - - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); - } - - public static void MigrateIntegrations() - { - // v2.2.0 - remove rbxfpsunlocker - string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); - - if (Directory.Exists(rbxfpsunlocker)) - Directory.Delete(rbxfpsunlocker, true); - - // v2.3.0 - remove reshade - string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); - string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); - - if (File.Exists(injectorLocation)) - { - App.ShowMessageBox( - "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ - "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", - MessageBoxImage.Warning - ); - - File.Delete(injectorLocation); - } - - if (File.Exists(configLocation)) - File.Delete(configLocation); - } - - private async Task ApplyModifications() - { - SetStatus("Applying Roblox modifications..."); - - // set executable flags for fullscreen optimizations - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - const string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; - string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); - - if (App.Settings.Prop.DisableFullscreenOptimizations) - { - if (appFlags is null) - appFlagsKey.SetValue(_playerLocation, $"~{flag}"); - else if (!appFlags.Contains(flag)) - appFlagsKey.SetValue(_playerLocation, appFlags + flag); - } - else if (appFlags is not null && appFlags.Contains(flag)) - { - // if there's more than one space, there's more flags set we need to preserve - if (appFlags.Split(' ').Length > 2) - appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); - else - appFlagsKey.DeleteValue(_playerLocation); - } - } - - // handle file mods - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); - string modFolder = Path.Combine(Directories.Modifications); - - // manifest has been moved to State.json - File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); - - List modFolderFiles = new(); - - if (!Directory.Exists(modFolder)) - Directory.CreateDirectory(modFolder); - - await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); - await CheckModPreset(App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"), @"ExtraContent\places\Mobile.rbxl", ""); - - // emoji presets are downloaded remotely from github due to how large they are - string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); - - if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) - { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - App.State.Prop.CurrentEmojiType = EmojiType.Default; - } - else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) - { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); - - var response = await App.HttpClient.GetAsync(remoteEmojiLocation); - await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - - App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; - } - - foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) - { - // get relative directory path - string relativeFile = file.Substring(modFolder.Length + 1); - - // v1.7.0 - README has been moved to the preferences menu now - if (relativeFile == "README.txt") - { - File.Delete(file); - continue; - } - - modFolderFiles.Add(relativeFile); - } - - // copy and overwrite - foreach (string file in modFolderFiles) - { - string fileModFolder = Path.Combine(modFolder, file); - string fileVersionFolder = Path.Combine(_versionFolder, file); - - if (File.Exists(fileVersionFolder)) - { - if (Utility.MD5Hash.FromFile(fileModFolder) == Utility.MD5Hash.FromFile(fileVersionFolder)) - continue; - } - - string? directory = Path.GetDirectoryName(fileVersionFolder); - - if (directory is null) - continue; - - Directory.CreateDirectory(directory); - - File.Copy(fileModFolder, fileVersionFolder, true); - File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); - } - - // the manifest is primarily here to keep track of what files have been - // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages - // now check for files that have been deleted from the mod folder according to the manifest - foreach (string fileLocation in App.State.Prop.ModManifest) - { - if (modFolderFiles.Contains(fileLocation)) - continue; - - KeyValuePair packageDirectory; - - try - { - packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); - } - catch (InvalidOperationException) - { - // package doesn't exist, likely mistakenly placed file - string versionFileLocation = Path.Combine(_versionFolder, fileLocation); - - if (File.Exists(versionFileLocation)) - File.Delete(versionFileLocation); - - continue; - } - - // restore original file - string fileName = fileLocation.Substring(packageDirectory.Value.Length); - ExtractFileFromPackage(packageDirectory.Key, fileName); - } - - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); - } - - private static async Task CheckModPreset(bool condition, string location, string name) - { - string modFolderLocation = Path.Combine(Directories.Modifications, location); - byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); - - if (condition) - { - if (!File.Exists(modFolderLocation)) - { - string? directory = Path.GetDirectoryName(modFolderLocation); - - if (directory is null) - return; - - Directory.CreateDirectory(directory); - - await File.WriteAllBytesAsync(modFolderLocation, binaryData); - } - } - else if (File.Exists(modFolderLocation) && Utility.MD5Hash.FromFile(modFolderLocation) == Utility.MD5Hash.FromBytes(binaryData)) - { - File.Delete(modFolderLocation); - } - } - - private async Task DownloadPackage(Package package) - { - if (_cancelFired) - return; - - string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); - - if (File.Exists(packageLocation)) - { - FileInfo file = new(packageLocation); - - string calculatedMD5 = Utility.MD5Hash.FromFile(packageLocation); - - if (calculatedMD5 != package.Signature) - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); - file.Delete(); - } - else - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is already downloaded, skipping..."); - _totalDownloadedBytes += package.PackedSize; - UpdateProgressbar(); - return; - } - } - else if (File.Exists(robloxPackageLocation)) - { - // let's cheat! if the stock bootstrapper already previously downloaded the file, - // then we can just copy the one from there - - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, packageLocation); - _totalDownloadedBytes += package.PackedSize; - UpdateProgressbar(); - return; - } - - if (!File.Exists(packageLocation)) - { - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Downloading {package.Name} ({package.Signature})..."); - - { - var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); - var buffer = new byte[4096]; - - await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); - - while (true) - { - if (_cancelFired) - { - stream.Close(); - fileStream.Close(); - return; - } - - var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, _cancelTokenSource.Token); - - if (bytesRead == 0) - break; // we're done - - await fileStream.WriteAsync(buffer, 0, bytesRead, _cancelTokenSource.Token); - - _totalDownloadedBytes += bytesRead; - UpdateProgressbar(); - } - } - - App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Finished downloading {package.Name}!"); - } - } - - private async Task ExtractPackage(Package package) - { - if (_cancelFired) - return; - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - string extractPath; - - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); - - using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation))) - { - foreach (ZipArchiveEntry entry in archive.Entries) - { - if (_cancelFired) - return; - - if (entry.FullName.EndsWith('\\')) - continue; - - extractPath = Path.Combine(packageFolder, entry.FullName); - - //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); - - string? directory = Path.GetDirectoryName(extractPath); - - if (directory is null) - continue; - - Directory.CreateDirectory(directory); - - await Task.Run(() => entry.ExtractToFile(extractPath, true)); - } - } - - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); - - _packagesExtracted += 1; - } - - private void ExtractFileFromPackage(string packageName, string fileName) - { - Package? package = _versionPackageManifest.Find(x => x.Name == packageName); - - if (package is null) - return; - - DownloadPackage(package).GetAwaiter().GetResult(); - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - - using ZipArchive archive = ZipFile.OpenRead(packageLocation); - - ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); - - if (entry is null) - return; - - string fileLocation = Path.Combine(packageFolder, entry.FullName); - - File.Delete(fileLocation); - - entry.ExtractToFile(fileLocation); - } - #endregion - } -} + } + + _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); + + if (App.Settings.Prop.Channel.ToLowerInvariant() != RobloxDeployment.DefaultChannel.ToLowerInvariant()) + _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLowerInvariant(); + + // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes + bool shouldWait = false; + + // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now + int gameClientPid; + using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) + { + gameClientPid = gameClient.Id; + } + + List autocloseProcesses = new(); + RobloxActivity? activityWatcher = null; + DiscordRichPresence? richPresence = null; + ServerNotifier? serverNotifier = null; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); + + using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) + { + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (!startEventFired) + return; + } + + if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) + { + activityWatcher = new(); + shouldWait = true; + } + + if (App.Settings.Prop.UseDiscordRichPresence) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); + richPresence = new(activityWatcher!); + } + + if (App.Settings.Prop.ShowServerDetails) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); + serverNotifier = new(activityWatcher!); + } + + // launch custom integrations now + foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); + + try + { + Process process = Process.Start(integration.Location, integration.LaunchArgs); + + if (integration.AutoClose) + { + shouldWait = true; + autocloseProcesses.Add(process); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); + } + } + + // event fired, wait for 3 seconds then close + await Task.Delay(3000); + Dialog?.CloseBootstrapper(); + + // keep bloxstrap open in the background if needed + if (!shouldWait) + return; + + activityWatcher?.StartWatcher(); + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); + + while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) + await Task.Delay(1000); + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); + + richPresence?.Dispose(); + + foreach (Process process in autocloseProcesses) + { + if (process.HasExited) + continue; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); + process.Kill(); + } + } + + public void CancelInstall() + { + if (!_isInstalling) + { + App.Terminate(ERROR_INSTALL_USEREXIT); + return; + } + + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); + + _cancelTokenSource.Cancel(); + _cancelFired = true; + + try + { + // clean up install + if (App.IsFirstRun) + Directory.Delete(Directories.Base, true); + else if (Directory.Exists(_versionFolder)) + Directory.Delete(_versionFolder, true); + } + catch (Exception ex) + { + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); + App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); + } + + App.Terminate(ERROR_INSTALL_USEREXIT); + } + #endregion + + #region App Install + public static void Register() + { + using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) + { + applicationKey.SetValue("InstallLocation", Directories.Base); + } + + // set uninstall key + using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) + { + uninstallKey.SetValue("DisplayIcon", $"{Directories.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", Directories.Base); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); + uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); + uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + } + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); + } + + public void RegisterProgramSize() + { + App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); + + using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); + + // sum compressed and uncompressed package sizes and convert to kilobytes + int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; + + uninstallKey.SetValue("EstimatedSize", totalSize); + + App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); + } + + private void CheckInstallMigration() + { + // check if we've changed our install location since the last time we started + // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff + + using RegistryKey? applicationKey = Registry.CurrentUser.OpenSubKey($@"Software\{App.ProjectName}", true); + + string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); + + if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) + return; + + SetStatus("Migrating install location..."); + + if (Directory.Exists(oldInstallLocation)) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); + + foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) + { + string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); + string newFileLocation = Path.Combine(Directories.Base, relativeFile); + string? newDirectory = Path.GetDirectoryName(newFileLocation); + + try + { + if (!String.IsNullOrEmpty(newDirectory)) + Directory.CreateDirectory(newDirectory); + + File.Move(oldFileLocation, newFileLocation, true); + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); + } + } + + try + { + Directory.Delete(oldInstallLocation, true); + App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); + } + } + + applicationKey.DeleteValue("OldInstallLocation"); + + // allow shortcuts to be re-registered + if (Directory.Exists(Directories.StartMenu)) + Directory.Delete(Directories.StartMenu, true); + + if (File.Exists(DesktopShortcutLocation)) + { + File.Delete(DesktopShortcutLocation); + App.Settings.Prop.CreateDesktopIcon = true; + } + + App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); + } + + public static void CheckInstall() + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); + + // check if launch uri is set to our bootstrapper + // this doesn't go under register, so we check every launch + // just in case the stock bootstrapper changes it back + + ProtocolHandler.Register("roblox", "Roblox", Directories.Application); + ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); + + // in case the user is reinstalling + if (File.Exists(Directories.Application) && App.IsFirstRun) + File.Delete(Directories.Application); + + // check to make sure bootstrapper is in the install folder + if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Directories.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(Directories.StartMenu)) + { + Directory.CreateDirectory(Directories.StartMenu); + + ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); + + ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); + } + else + { + // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" + string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); + string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); + + if (File.Exists(oldMenuShortcut)) + File.Delete(oldMenuShortcut); + + if (!File.Exists(newMenuShortcut)) + ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + .WriteToFile(newMenuShortcut); + } + + if (App.Settings.Prop.CreateDesktopIcon) + { + if (!File.Exists(DesktopShortcutLocation)) + { + ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + .WriteToFile(DesktopShortcutLocation); + } + + // one-time toggle, set it back to false + App.Settings.Prop.CreateDesktopIcon = false; + } + } + + private async Task CheckForUpdates() + { + // don't update if there's another instance running (likely running in the background) + if (Process.GetProcessesByName(App.ProjectName).Count() > 1) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); + return; + } + + string currentVersion = $"{App.ProjectName} v{App.Version}"; + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); + + var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); + + if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + return; + } + + SetStatus($"Getting the latest {App.ProjectName}..."); + + // 64-bit is always the first option + GithubReleaseAsset asset = releaseInfo.Assets[0]; + string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); + + if (!File.Exists(downloadLocation)) + { + var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + + await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + } + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); + + ProcessStartInfo startInfo = new() + { + FileName = downloadLocation, + }; + + foreach (string arg in App.LaunchArgs) + startInfo.ArgumentList.Add(arg); + + App.Settings.Save(); + + Process.Start(startInfo); + + Environment.Exit(0); + } + + private void Uninstall() + { + // prompt to shutdown roblox if its currently running + if (Process.GetProcessesByName(App.RobloxAppName).Any()) + { + App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); + + Dialog?.PromptShutdown(); + + try + { + foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) + { + process.CloseMainWindow(); + process.Close(); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); + } + + App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); + } + + SetStatus($"Uninstalling {App.ProjectName}..."); + + //App.Settings.ShouldSave = false; + App.ShouldSaveConfigs = false; + + // check if stock bootstrapper is still installed + RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + if (bootstrapperKey is null) + { + 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); + } + + try + { + // delete application key + Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"); + + // delete start menu folder + Directory.Delete(Directories.StartMenu, true); + + // delete desktop shortcut + File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); + + // delete uninstall key + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); + + // delete installation folder + // (should delete everything except bloxstrap itself) + Directory.Delete(Directories.Base, true); + } + catch (Exception ex) + { + App.Logger.WriteLine($"Could not fully uninstall! ({ex})"); + } + + Action? callback = null; + + if (Directory.Exists(Directories.Base)) + { + callback = () => + { + // 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" + + Process.Start(new ProcessStartInfo() + { + FileName = "cmd.exe", + Arguments = $"/c timeout 5 && del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\"", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + }; + } + + Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled", callback); + } + #endregion + + #region Roblox Install + private async Task InstallLatestVersion() + { + _isInstalling = true; + + SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); + + Directory.CreateDirectory(Directories.Base); + Directory.CreateDirectory(Directories.Downloads); + Directory.CreateDirectory(Directories.Versions); + + // package manifest states packed size and uncompressed size in exact bytes + // packed size only matters if we don't already have the package cached on disk + string[] cachedPackages = Directory.GetFiles(Directories.Downloads); + int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); + + if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) + { + App.ShowMessageBox($"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); + App.Terminate(ERROR_INSTALL_FAILURE); + return; + } + + if (Dialog is not null) + { + Dialog.CancelEnabled = true; + Dialog.ProgressStyle = ProgressBarStyle.Continuous; + } + + // compute total bytes to download + _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); + + foreach (Package package in _versionPackageManifest) + { + if (_cancelFired) + return; + + // download all the packages synchronously + await DownloadPackage(package); + + // we'll extract the runtime installer later if we need to + if (package.Name == "WebView2RuntimeInstaller.zip") + continue; + + // extract the package immediately after download asynchronously + // discard is just used to suppress the warning + Task _ = ExtractPackage(package); + } + + if (_cancelFired) + return; + + // allow progress bar to 100% before continuing (purely ux reasons lol) + await Task.Delay(1000); + + if (Dialog is not null) + { + Dialog.ProgressStyle = ProgressBarStyle.Marquee; + SetStatus("Configuring Roblox..."); + } + + // wait for all packages to finish extracting, with an exception for the webview2 runtime installer + while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) + { + await Task.Delay(100); + } + + string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); + await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + + if (_cancelFired) + return; + + if (!FreshInstall) + { + // let's take this opportunity to delete any packages we don't need anymore + foreach (string filename in cachedPackages) + { + if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) + { + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); + File.Delete(filename); + } + } + + string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); + + // move old compatibility flags for the old location + using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) + { + string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); + string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); + + if (appFlags is not null) + { + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); + appFlagsKey.SetValue(_playerLocation, appFlags); + appFlagsKey.DeleteValue(oldGameClientLocation); + } + } + + // delete any old version folders + // we only do this if roblox isnt running just in case an update happened + // while they were launching a second instance or something idk + if (!Process.GetProcessesByName(App.RobloxAppName).Any()) + { + foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) + { + if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) + continue; + + App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); + dir.Delete(true); + } + } + } + + App.State.Prop.VersionGuid = _latestVersionGuid; + + // don't register program size until the program is registered, which will be done after this + if (!App.IsFirstRun && !FreshInstall) + RegisterProgramSize(); + + if (Dialog is not null) + Dialog.CancelEnabled = false; + + _isInstalling = false; + } + + private async Task 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($"[Bootstrapper::InstallWebView2] Installing runtime..."); + + string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); + + if (!Directory.Exists(baseDirectory)) + { + Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); + + if (package is null) + { + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); + return; + } + + await ExtractPackage(package); + } + + SetStatus("Installing WebView2, please wait..."); + + ProcessStartInfo startInfo = new() + { + WorkingDirectory = baseDirectory, + FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), + Arguments = "/silent /install" + }; + + await Process.Start(startInfo)!.WaitForExitAsync(); + + App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); + } + + public static void MigrateIntegrations() + { + // v2.2.0 - remove rbxfpsunlocker + string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); + + if (Directory.Exists(rbxfpsunlocker)) + Directory.Delete(rbxfpsunlocker, true); + + // v2.3.0 - remove reshade + string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); + string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); + + if (File.Exists(injectorLocation)) + { + App.ShowMessageBox( + "Roblox has now finished rolling out the new game client update, featuring 64-bit support and the Hyperion anticheat. ReShade does not work with this update, and so it has now been disabled and removed from Bloxstrap.\n\n"+ + "Your ReShade configuration files will still be saved, and you can locate them by opening the folder where Bloxstrap is installed to, and navigating to the Integrations folder. You can choose to delete these if you want.", + MessageBoxImage.Warning + ); + + File.Delete(injectorLocation); + } + + if (File.Exists(configLocation)) + File.Delete(configLocation); + } + + private async Task ApplyModifications() + { + SetStatus("Applying Roblox modifications..."); + + // set executable flags for fullscreen optimizations + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); + using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) + { + const string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; + string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); + + if (App.Settings.Prop.DisableFullscreenOptimizations) + { + if (appFlags is null) + appFlagsKey.SetValue(_playerLocation, $"~{flag}"); + else if (!appFlags.Contains(flag)) + appFlagsKey.SetValue(_playerLocation, appFlags + flag); + } + else if (appFlags is not null && appFlags.Contains(flag)) + { + // if there's more than one space, there's more flags set we need to preserve + if (appFlags.Split(' ').Length > 2) + appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); + else + appFlagsKey.DeleteValue(_playerLocation); + } + } + + // handle file mods + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); + string modFolder = Path.Combine(Directories.Modifications); + + // manifest has been moved to State.json + File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); + + List modFolderFiles = new(); + + if (!Directory.Exists(modFolder)) + Directory.CreateDirectory(modFolder); + + await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); + await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); + await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"), @"ExtraContent\places\Mobile.rbxl", ""); + + // emoji presets are downloaded remotely from github due to how large they are + string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); + + if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + App.State.Prop.CurrentEmojiType = EmojiType.Default; + } + else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) + { + if (File.Exists(emojiFontLocation)) + File.Delete(emojiFontLocation); + + string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); + + var response = await App.HttpClient.GetAsync(remoteEmojiLocation); + await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + + App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; + } + + foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) + { + // get relative directory path + string relativeFile = file.Substring(modFolder.Length + 1); + + // v1.7.0 - README has been moved to the preferences menu now + if (relativeFile == "README.txt") + { + File.Delete(file); + continue; + } + + modFolderFiles.Add(relativeFile); + } + + // copy and overwrite + foreach (string file in modFolderFiles) + { + string fileModFolder = Path.Combine(modFolder, file); + string fileVersionFolder = Path.Combine(_versionFolder, file); + + if (File.Exists(fileVersionFolder)) + { + if (Utility.MD5Hash.FromFile(fileModFolder) == Utility.MD5Hash.FromFile(fileVersionFolder)) + continue; + } + + string? directory = Path.GetDirectoryName(fileVersionFolder); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + File.Copy(fileModFolder, fileVersionFolder, true); + File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); + } + + // the manifest is primarily here to keep track of what files have been + // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages + // now check for files that have been deleted from the mod folder according to the manifest + foreach (string fileLocation in App.State.Prop.ModManifest) + { + if (modFolderFiles.Contains(fileLocation)) + continue; + + KeyValuePair packageDirectory; + + try + { + packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + } + catch (InvalidOperationException) + { + // package doesn't exist, likely mistakenly placed file + string versionFileLocation = Path.Combine(_versionFolder, fileLocation); + + if (File.Exists(versionFileLocation)) + File.Delete(versionFileLocation); + + continue; + } + + // restore original file + string fileName = fileLocation.Substring(packageDirectory.Value.Length); + ExtractFileFromPackage(packageDirectory.Key, fileName); + } + + App.State.Prop.ModManifest = modFolderFiles; + App.State.Save(); + } + + private static async Task CheckModPreset(bool condition, string location, string name) + { + string modFolderLocation = Path.Combine(Directories.Modifications, location); + byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); + + if (condition) + { + if (!File.Exists(modFolderLocation)) + { + string? directory = Path.GetDirectoryName(modFolderLocation); + + if (directory is null) + return; + + Directory.CreateDirectory(directory); + + await File.WriteAllBytesAsync(modFolderLocation, binaryData); + } + } + else if (File.Exists(modFolderLocation) && Utility.MD5Hash.FromFile(modFolderLocation) == Utility.MD5Hash.FromBytes(binaryData)) + { + File.Delete(modFolderLocation); + } + } + + private async Task DownloadPackage(Package package) + { + if (_cancelFired) + return; + + string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); + + if (File.Exists(packageLocation)) + { + FileInfo file = new(packageLocation); + + string calculatedMD5 = Utility.MD5Hash.FromFile(packageLocation); + + if (calculatedMD5 != package.Signature) + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); + file.Delete(); + } + else + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] {package.Name} is already downloaded, skipping..."); + _totalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + } + else if (File.Exists(robloxPackageLocation)) + { + // let's cheat! if the stock bootstrapper already previously downloaded the file, + // then we can just copy the one from there + + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); + File.Copy(robloxPackageLocation, packageLocation); + _totalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + + if (!File.Exists(packageLocation)) + { + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Downloading {package.Name} ({package.Signature})..."); + + { + var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); + var buffer = new byte[4096]; + + await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); + await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.Write, FileShare.Delete); + + while (true) + { + if (_cancelFired) + { + stream.Close(); + fileStream.Close(); + return; + } + + var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, _cancelTokenSource.Token); + + if (bytesRead == 0) + break; // we're done + + await fileStream.WriteAsync(buffer, 0, bytesRead, _cancelTokenSource.Token); + + _totalDownloadedBytes += bytesRead; + UpdateProgressbar(); + } + } + + App.Logger.WriteLine($"[Bootstrapper::DownloadPackage] Finished downloading {package.Name}!"); + } + } + + private async Task ExtractPackage(Package package) + { + if (_cancelFired) + return; + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + string extractPath; + + App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); + + using (ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation))) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (_cancelFired) + return; + + if (entry.FullName.EndsWith('\\')) + continue; + + extractPath = Path.Combine(packageFolder, entry.FullName); + + //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); + + string? directory = Path.GetDirectoryName(extractPath); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + await Task.Run(() => entry.ExtractToFile(extractPath, true)); + } + } + + App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); + + _packagesExtracted += 1; + } + + private void ExtractFileFromPackage(string packageName, string fileName) + { + Package? package = _versionPackageManifest.Find(x => x.Name == packageName); + + if (package is null) + return; + + DownloadPackage(package).GetAwaiter().GetResult(); + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + + using ZipArchive archive = ZipFile.OpenRead(packageLocation); + + ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); + + if (entry is null) + return; + + string fileLocation = Path.Combine(packageFolder, entry.FullName); + + File.Delete(fileLocation); + + entry.ExtractToFile(fileLocation); + } + #endregion + } +} From 2258000a89ee8aa565f71f7d076622b3ffbdf3b2 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 27 Jun 2023 23:57:57 +0100 Subject: [PATCH 066/111] Add build metadata for diagnostics, update checks is this actually gonna work? uhhhh maybe idk --- .github/workflows/ci.yml | 15 +++++--- Bloxstrap/App.xaml.cs | 7 ++++ Bloxstrap/Bloxstrap.csproj | 9 +++++ Bloxstrap/Bootstrapper.cs | 19 +++++++--- Bloxstrap/Extensions/DateTimeEx.cs | 12 +++++++ Bloxstrap/Logger.cs | 4 +-- .../Attributes/CompileTimeInfoAttribute.cs | 21 +++++++++++ .../UI/Menu/ViewModels/AboutViewModel.cs | 16 ++++++++- .../Menu/ViewModels/InstallationViewModel.cs | 3 +- Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml | 35 +++++++++++++++++++ Bloxstrap/Updater.cs | 4 +-- Bloxstrap/Utilities.cs | 7 ++++ 12 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 Bloxstrap/Extensions/DateTimeEx.cs create mode 100644 Bloxstrap/Models/Attributes/CompileTimeInfoAttribute.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1921310..294c81f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,28 +6,33 @@ jobs: strategy: matrix: configuration: [Debug, Release] - platform: [x64] + runs-on: windows-latest steps: - uses: actions/checkout@v3 with: submodules: true + - uses: actions/setup-dotnet@v3 with: dotnet-version: '6.x' + - name: Restore dependencies run: dotnet restore + - name: Build run: dotnet build --no-restore + - name: Publish - run: dotnet publish -p:PublishSingleFile=true -r win-${{ matrix.platform }} -c ${{ matrix.configuration }} --self-contained false .\Bloxstrap\Bloxstrap.csproj + run: dotnet publish -p:PublishSingleFile=true -p:CommitHash=${{ github.sha }} -p:CommitRef=${{ github.ref_type }}/${{ github.ref_name }} -r win-x64 -c ${{ matrix.configuration }} --self-contained false .\Bloxstrap\Bloxstrap.csproj + - name: Upload Artifact uses: actions/upload-artifact@v3 with: - name: Bloxstrap (${{ matrix.configuration }}, ${{ matrix.platform }}) + name: Bloxstrap (${{ matrix.configuration }}, x64) path: | - .\Bloxstrap\bin\${{ matrix.configuration }}\net6.0-windows\win-${{ matrix.platform }}\publish\* + .\Bloxstrap\bin\${{ matrix.configuration }}\net6.0-windows\win-x64\publish\* release: needs: build @@ -40,9 +45,11 @@ jobs: with: name: Bloxstrap (Release, x64) path: x64 + - name: Rename binaries run: | mv x64/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}-x64.exe + - name: Release uses: softprops/action-gh-release@v1 with: diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index d2c1cd0..1b5a504 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -15,6 +15,7 @@ using Microsoft.Win32; using Bloxstrap.Extensions; using Bloxstrap.Models; +using Bloxstrap.Models.Attributes; using Bloxstrap.UI.BootstrapperDialogs; using Bloxstrap.UI.Menu.Views; using Bloxstrap.Utility; @@ -44,6 +45,7 @@ namespace Bloxstrap public static bool IsMenuLaunch { get; private set; } = false; public static string[] LaunchArgs { get; private set; } = null!; + public static BuildMetadataAttribute BuildMetadata => Assembly.GetExecutingAssembly().GetCustomAttribute()!; public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; // singletons @@ -118,6 +120,11 @@ namespace Bloxstrap Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}"); + if (String.IsNullOrEmpty(BuildMetadata.CommitHash)) + Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}"); + else + Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})"); + // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 24f4203..c4cccd6 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -39,4 +39,13 @@ + + + <_Parameter1>$([System.DateTime]::UtcNow.ToString("s"))Z + <_Parameter2>$(COMPUTERNAME)\$(USERNAME) + <_Parameter3>$(CommitHash) + <_Parameter4>$(CommitRef) + + + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 5c17080..2676e9c 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -576,25 +576,34 @@ namespace Bloxstrap return; } - string currentVersion = $"{App.ProjectName} v{App.Version}"; - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) + if (releaseInfo is null || releaseInfo.Assets is null) { App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); return; } + int numCurrentVersion = Utilities.VersionToNumber(App.Version); + int numLatestVersion = Utilities.VersionToNumber(releaseInfo.TagName); + + // check if we aren't using a deployed build, so we can update to one if a new version comes out + if (numCurrentVersion == numLatestVersion && App.BuildMetadata.CommitRef.StartsWith("tag") || numCurrentVersion > numLatestVersion) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + return; + } + + SetStatus($"Getting the latest {App.ProjectName}..."); // 64-bit is always the first option GithubReleaseAsset asset = releaseInfo.Assets[0]; string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.TagName}..."); if (!File.Exists(downloadLocation)) { @@ -604,7 +613,7 @@ namespace Bloxstrap await response.Content.CopyToAsync(fileStream); } - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.TagName}..."); ProcessStartInfo startInfo = new() { diff --git a/Bloxstrap/Extensions/DateTimeEx.cs b/Bloxstrap/Extensions/DateTimeEx.cs new file mode 100644 index 0000000..f30d011 --- /dev/null +++ b/Bloxstrap/Extensions/DateTimeEx.cs @@ -0,0 +1,12 @@ +using System; + +namespace Bloxstrap.Extensions +{ + static class DateTimeEx + { + public static string ToFriendlyString(this DateTime dateTime) + { + return dateTime.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", App.CultureFormat); + } + } +} diff --git a/Bloxstrap/Logger.cs b/Bloxstrap/Logger.cs index ad4bc93..883dac2 100644 --- a/Bloxstrap/Logger.cs +++ b/Bloxstrap/Logger.cs @@ -39,9 +39,9 @@ namespace Bloxstrap public void WriteLine(string message) { - string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'"); + string timestamp = DateTime.UtcNow.ToString("s") + "Z"; string outcon = $"{timestamp} {message}"; - string outlog = outcon.Replace(Directories.UserProfile, ""); + string outlog = outcon.Replace(Directories.UserProfile, "%UserProfile%"); Debug.WriteLine(outcon); WriteToLog(outlog); diff --git a/Bloxstrap/Models/Attributes/CompileTimeInfoAttribute.cs b/Bloxstrap/Models/Attributes/CompileTimeInfoAttribute.cs new file mode 100644 index 0000000..32423f3 --- /dev/null +++ b/Bloxstrap/Models/Attributes/CompileTimeInfoAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace Bloxstrap.Models.Attributes +{ + [AttributeUsage(AttributeTargets.Assembly)] + public class BuildMetadataAttribute : Attribute + { + public DateTime Timestamp { get; set; } + public string Machine { get; set; } + public string CommitHash { get; set; } + public string CommitRef { get; set; } + + public BuildMetadataAttribute(string timestamp, string machine, string commitHash, string commitRef) + { + Timestamp = DateTime.Parse(timestamp).ToLocalTime(); + Machine = machine; + CommitHash = commitHash; + CommitRef = commitRef; + } + } +} diff --git a/Bloxstrap/UI/Menu/ViewModels/AboutViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/AboutViewModel.cs index 89566dc..cea0d43 100644 --- a/Bloxstrap/UI/Menu/ViewModels/AboutViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/AboutViewModel.cs @@ -1,7 +1,21 @@ -namespace Bloxstrap.UI.Menu.ViewModels +using System; +using System.Windows; + +using Bloxstrap.Extensions; +using Bloxstrap.Models.Attributes; + +namespace Bloxstrap.UI.Menu.ViewModels { public class AboutViewModel { public string Version => $"Version {App.Version}"; + + public BuildMetadataAttribute BuildMetadata => App.BuildMetadata; + + public string BuildTimestamp => BuildMetadata.Timestamp.ToFriendlyString(); + public string BuildCommitHashUrl => $"https://github.com/{App.ProjectRepository}/commit/{BuildMetadata.CommitHash}"; + + public Visibility BuildInformationVisibility => BuildMetadata.CommitRef.StartsWith("tag") ? Visibility.Collapsed : Visibility.Visible; + public Visibility BuildCommitVisibility => String.IsNullOrEmpty(BuildMetadata.CommitHash) ? Visibility.Collapsed : Visibility.Visible; } } diff --git a/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs index e3ca027..cfd75b5 100644 --- a/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs @@ -12,6 +12,7 @@ using System.Windows.Input; using CommunityToolkit.Mvvm.Input; using Bloxstrap.Enums; +using Bloxstrap.Extensions; using Bloxstrap.Models; namespace Bloxstrap.UI.Menu.ViewModels @@ -50,7 +51,7 @@ namespace Bloxstrap.UI.Menu.ViewModels { Version = info.Version, VersionGuid = info.VersionGuid, - Timestamp = info.Timestamp?.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", App.CultureFormat)! + Timestamp = info.Timestamp?.ToFriendlyString()! }; OnPropertyChanged(nameof(ChannelDeployInfo)); diff --git a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml index 2a6f0bc..e2a3d09 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml @@ -41,6 +41,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/Updater.cs b/Bloxstrap/Updater.cs index d229f1c..e9d53a4 100644 --- a/Bloxstrap/Updater.cs +++ b/Bloxstrap/Updater.cs @@ -19,11 +19,9 @@ namespace Bloxstrap // 2.0.0 downloads updates to /Updates so lol bool isAutoUpgrade = Environment.ProcessPath.StartsWith(Path.Combine(Directories.Base, "Updates")) || Environment.ProcessPath.StartsWith(Path.Combine(Directories.LocalAppData, "Temp")); - // if downloaded version doesn't match, replace installed version with downloaded version FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath); - FileVersionInfo installedVersionInfo = FileVersionInfo.GetVersionInfo(Directories.Application); - if (installedVersionInfo.ProductVersion == currentVersionInfo.ProductVersion) + if (Utility.MD5Hash.FromFile(Environment.ProcessPath) == Utility.MD5Hash.FromFile(Directories.Application)) return; MessageBoxResult result; diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 53571d9..4371146 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -39,5 +39,12 @@ namespace Bloxstrap return default; } } + + public static int VersionToNumber(string version) + { + // yes this is kinda stupid lol + version = version.Replace("v", "").Replace(".", ""); + return Int32.Parse(version); + } } } From 3a77746daa1a97c961d36444af1a81a034e3f676 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 28 Jun 2023 00:08:25 +0100 Subject: [PATCH 067/111] Remove reference for IniParser --- Bloxstrap/Bloxstrap.csproj | 1 - Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index c4cccd6..ddc7eaa 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -31,7 +31,6 @@ - diff --git a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml index e2a3d09..1f398d8 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml @@ -183,9 +183,9 @@ - + - + @@ -195,14 +195,6 @@ - - - - - - - - From fe885cb24be496e1ee1f4ab1951ef02c57b27a00 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 28 Jun 2023 00:08:57 +0100 Subject: [PATCH 068/111] Bump version number --- Bloxstrap/Bloxstrap.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index ddc7eaa..c2e0c57 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.3.0 - 2.3.0.0 + 2.4.0 + 2.4.0.0 app.manifest From 991005c6a702506b86fa47990987ea6bc7d1788e Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 28 Jun 2023 20:57:23 +0100 Subject: [PATCH 069/111] Improve channel input selection handling input value is now trimmed, as well as input being processed on enter --- .../Menu/ViewModels/InstallationViewModel.cs | 6 ++--- .../UI/Menu/Views/Pages/InstallationPage.xaml | 2 +- .../Menu/Views/Pages/InstallationPage.xaml.cs | 23 ++++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs b/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs index cfd75b5..3b5c020 100644 --- a/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs +++ b/Bloxstrap/UI/Menu/ViewModels/InstallationViewModel.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; -using System.Windows.Forms; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; @@ -65,9 +64,9 @@ namespace Bloxstrap.UI.Menu.ViewModels private void BrowseInstallLocation() { - using var dialog = new FolderBrowserDialog(); + using var dialog = new System.Windows.Forms.FolderBrowserDialog(); - if (dialog.ShowDialog() != DialogResult.OK) + if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; if (!dialog.SelectedPath.EndsWith(App.ProjectName)) @@ -96,6 +95,7 @@ namespace Bloxstrap.UI.Menu.ViewModels get => App.Settings.Prop.Channel; set { + value = value.Trim(); Task.Run(() => LoadChannelDeployInfo(value)); App.Settings.Prop.Channel = value; } diff --git a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml index 81a9bac..dbce9db 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml @@ -59,7 +59,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml.cs b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml.cs index 19c7d32..84d03e5 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml.cs +++ b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml.cs @@ -1,4 +1,9 @@ -using Bloxstrap.UI.Menu.ViewModels; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; + +using Bloxstrap.UI.Menu.ViewModels; namespace Bloxstrap.UI.Menu.Views.Pages { @@ -12,5 +17,21 @@ namespace Bloxstrap.UI.Menu.Views.Pages DataContext = new InstallationViewModel(); InitializeComponent(); } + + // https://stackoverflow.com/a/13289118/11852173 + // yes this doesnt fully conform to xaml but whatever + private void TextBox_KeyEnterUpdate(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + TextBox tBox = (TextBox)sender; + DependencyProperty prop = TextBox.TextProperty; + + BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop); + + if (binding is not null) + binding.UpdateSource(); + } + } } } From d27ca05c32e6560b95c7e9893dfbbd5b67df3592 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Wed, 28 Jun 2023 21:42:50 +0100 Subject: [PATCH 070/111] Move altered padding to wpfui --- Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml | 10 +++++----- .../UI/Menu/Views/Pages/AppearancePage.xaml | 8 ++++---- .../UI/Menu/Views/Pages/BehaviourPage.xaml | 4 ++-- .../UI/Menu/Views/Pages/FastFlagsPage.xaml | 16 ++++++++-------- .../UI/Menu/Views/Pages/InstallationPage.xaml | 8 ++++---- .../UI/Menu/Views/Pages/IntegrationsPage.xaml | 8 ++++---- Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml | 18 ++++++++++-------- wpfui | 2 +- 8 files changed, 38 insertions(+), 36 deletions(-) diff --git a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml index 1f398d8..afd2b74 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/AboutPage.xaml @@ -165,31 +165,31 @@ - + - + - + - + - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml b/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml index b5b2c4c..48e9be1 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/AppearancePage.xaml @@ -12,7 +12,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -50,7 +50,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml index e0d7ff4..190a539 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/BehaviourPage.xaml @@ -11,7 +11,7 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml index d7a130e..6d69573 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/FastFlagsPage.xaml @@ -12,7 +12,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -51,7 +51,7 @@ - + @@ -60,7 +60,7 @@ - + @@ -69,7 +69,7 @@ - + @@ -80,7 +80,7 @@ - + @@ -89,7 +89,7 @@ - + @@ -98,7 +98,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml index dbce9db..99cb18d 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/InstallationPage.xaml @@ -12,7 +12,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -121,7 +121,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml index 234dfd0..db022b2 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/IntegrationsPage.xaml @@ -13,7 +13,7 @@ - + @@ -22,7 +22,7 @@ - + @@ -33,7 +33,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml index 7c801c4..4b66f38 100644 --- a/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Menu/Views/Pages/ModsPage.xaml @@ -21,7 +21,7 @@ - + @@ -35,12 +35,14 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs index f423e1b..a1627f7 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs @@ -1,4 +1,9 @@ -using Bloxstrap.UI.ViewModels.Menu; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; + +using Bloxstrap.UI.ViewModels.Menu; namespace Bloxstrap.UI.Menu.Pages { @@ -12,5 +17,21 @@ namespace Bloxstrap.UI.Menu.Pages DataContext = new BehaviourViewModel(); InitializeComponent(); } + + // https://stackoverflow.com/a/13289118/11852173 + // yes this doesnt fully conform to xaml but whatever + private void TextBox_KeyEnterUpdate(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + TextBox tBox = (TextBox)sender; + DependencyProperty prop = TextBox.TextProperty; + + BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop); + + if (binding is not null) + binding.UpdateSource(); + } + } } } diff --git a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml index 510f8a8..b478e7b 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml @@ -10,7 +10,7 @@ Title="InstallationPage" Scrollable="True"> - + @@ -46,89 +46,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml.cs index 7bf5b8b..1a21b6d 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml.cs @@ -1,9 +1,4 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Input; - -using Bloxstrap.UI.ViewModels.Menu; +using Bloxstrap.UI.ViewModels.Menu; namespace Bloxstrap.UI.Menu.Pages { @@ -17,21 +12,5 @@ namespace Bloxstrap.UI.Menu.Pages DataContext = new InstallationViewModel(); InitializeComponent(); } - - // https://stackoverflow.com/a/13289118/11852173 - // yes this doesnt fully conform to xaml but whatever - private void TextBox_KeyEnterUpdate(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - TextBox tBox = (TextBox)sender; - DependencyProperty prop = TextBox.TextProperty; - - BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop); - - if (binding is not null) - binding.UpdateSource(); - } - } } } diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs index d5fb042..78661f3 100644 --- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs @@ -1,7 +1,60 @@ -namespace Bloxstrap.UI.ViewModels.Menu +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + + +using Bloxstrap.Enums; +using Bloxstrap.Extensions; +using Bloxstrap.Models; + +namespace Bloxstrap.UI.ViewModels.Menu { - public class BehaviourViewModel + public class BehaviourViewModel : INotifyPropertyChanged { + public event PropertyChangedEventHandler? PropertyChanged; + public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + private bool _manualChannelEntry = !RobloxDeployment.SelectableChannels.Contains(App.Settings.Prop.Channel); + + public BehaviourViewModel() + { + Task.Run(() => LoadChannelDeployInfo(App.Settings.Prop.Channel)); + } + + private async Task LoadChannelDeployInfo(string channel) + { + ChannelInfoLoadingText = "Fetching latest deploy info, please wait..."; + OnPropertyChanged(nameof(ChannelInfoLoadingText)); + + ChannelDeployInfo = null; + OnPropertyChanged(nameof(ChannelDeployInfo)); + + try + { + ClientVersion info = await RobloxDeployment.GetInfo(channel, true); + + ChannelDeployInfo = new DeployInfo + { + Version = info.Version, + VersionGuid = info.VersionGuid, + Timestamp = info.Timestamp?.ToFriendlyString()! + }; + + OnPropertyChanged(nameof(ChannelDeployInfo)); + } + catch (Exception) + { + ChannelInfoLoadingText = "Failed to get deploy info.\nIs the channel name valid?"; + OnPropertyChanged(nameof(ChannelInfoLoadingText)); + } + } + + public DeployInfo? ChannelDeployInfo { get; private set; } = null; + public string ChannelInfoLoadingText { get; private set; } = null!; + public bool CreateDesktopIcon { get => App.Settings.Prop.CreateDesktopIcon; @@ -13,5 +66,56 @@ get => App.Settings.Prop.CheckForUpdates; set => App.Settings.Prop.CheckForUpdates = value; } + + public IEnumerable Channels => RobloxDeployment.SelectableChannels; + + public string Channel + { + get => App.Settings.Prop.Channel; + set + { + value = value.Trim(); + Task.Run(() => LoadChannelDeployInfo(value)); + App.Settings.Prop.Channel = value; + } + } + + public bool ManualChannelEntry + { + get => _manualChannelEntry; + set + { + _manualChannelEntry = value; + + if (!value) + { + // roblox typically sets channels in all lowercase, so here we find if a case insensitive match exists + string? matchingChannel = Channels.Where(x => x.ToLowerInvariant() == Channel.ToLowerInvariant()).FirstOrDefault(); + Channel = string.IsNullOrEmpty(matchingChannel) ? RobloxDeployment.DefaultChannel : matchingChannel; + } + + OnPropertyChanged(nameof(Channel)); + OnPropertyChanged(nameof(ChannelComboBoxVisibility)); + OnPropertyChanged(nameof(ChannelTextBoxVisibility)); + } + } + + // cant use data bindings so i have to do whatever tf this is + public Visibility ChannelComboBoxVisibility => ManualChannelEntry ? Visibility.Collapsed : Visibility.Visible; + public Visibility ChannelTextBoxVisibility => ManualChannelEntry ? Visibility.Visible : Visibility.Collapsed; + + // todo - move to enum attributes? + public IReadOnlyDictionary ChannelChangeModes => new Dictionary + { + { "Change automatically", ChannelChangeMode.Automatic }, + { "Always prompt", ChannelChangeMode.Prompt }, + { "Never change", ChannelChangeMode.Ignore }, + }; + + public string SelectedChannelChangeMode + { + get => ChannelChangeModes.FirstOrDefault(x => x.Value == App.Settings.Prop.ChannelChangeMode).Key; + set => App.Settings.Prop.ChannelChangeMode = ChannelChangeModes[value]; + } } } diff --git a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs index 43d33eb..5a659f7 100644 --- a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs @@ -1,19 +1,9 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; -using Bloxstrap.Models; - namespace Bloxstrap.UI.ViewModels.Menu { public class InstallationViewModel : INotifyPropertyChanged @@ -21,47 +11,9 @@ namespace Bloxstrap.UI.ViewModels.Menu public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private bool _manualChannelEntry = !RobloxDeployment.SelectableChannels.Contains(App.Settings.Prop.Channel); - public ICommand BrowseInstallLocationCommand => new RelayCommand(BrowseInstallLocation); public ICommand OpenFolderCommand => new RelayCommand(OpenFolder); - public DeployInfo? ChannelDeployInfo { get; private set; } = null; - public string ChannelInfoLoadingText { get; private set; } = null!; - - public InstallationViewModel() - { - Task.Run(() => LoadChannelDeployInfo(App.Settings.Prop.Channel)); - } - - private async Task LoadChannelDeployInfo(string channel) - { - ChannelInfoLoadingText = "Fetching latest deploy info, please wait..."; - OnPropertyChanged(nameof(ChannelInfoLoadingText)); - - ChannelDeployInfo = null; - OnPropertyChanged(nameof(ChannelDeployInfo)); - - try - { - ClientVersion info = await RobloxDeployment.GetInfo(channel, true); - - ChannelDeployInfo = new DeployInfo - { - Version = info.Version, - VersionGuid = info.VersionGuid, - Timestamp = info.Timestamp?.ToFriendlyString()! - }; - - OnPropertyChanged(nameof(ChannelDeployInfo)); - } - catch (Exception) - { - ChannelInfoLoadingText = "Failed to get deploy info.\nIs the channel name valid?"; - OnPropertyChanged(nameof(ChannelInfoLoadingText)); - } - } - private void BrowseInstallLocation() { using var dialog = new System.Windows.Forms.FolderBrowserDialog(); @@ -84,56 +36,5 @@ namespace Bloxstrap.UI.ViewModels.Menu get => App.BaseDirectory; set => App.BaseDirectory = value; } - - public IEnumerable Channels => RobloxDeployment.SelectableChannels; - - public string Channel - { - get => App.Settings.Prop.Channel; - set - { - value = value.Trim(); - Task.Run(() => LoadChannelDeployInfo(value)); - App.Settings.Prop.Channel = value; - } - } - - public bool ManualChannelEntry - { - get => _manualChannelEntry; - set - { - _manualChannelEntry = value; - - if (!value) - { - // roblox typically sets channels in all lowercase, so here we find if a case insensitive match exists - string? matchingChannel = Channels.Where(x => x.ToLowerInvariant() == Channel.ToLowerInvariant()).FirstOrDefault(); - Channel = string.IsNullOrEmpty(matchingChannel) ? RobloxDeployment.DefaultChannel : matchingChannel; - } - - OnPropertyChanged(nameof(Channel)); - OnPropertyChanged(nameof(ChannelComboBoxVisibility)); - OnPropertyChanged(nameof(ChannelTextBoxVisibility)); - } - } - - // cant use data bindings so i have to do whatever tf this is - public Visibility ChannelComboBoxVisibility => ManualChannelEntry ? Visibility.Collapsed : Visibility.Visible; - public Visibility ChannelTextBoxVisibility => ManualChannelEntry ? Visibility.Visible : Visibility.Collapsed; - - // todo - move to enum attributes? - public IReadOnlyDictionary ChannelChangeModes => new Dictionary - { - { "Change automatically", ChannelChangeMode.Automatic }, - { "Always prompt", ChannelChangeMode.Prompt }, - { "Never change", ChannelChangeMode.Ignore }, - }; - - public string SelectedChannelChangeMode - { - get => ChannelChangeModes.FirstOrDefault(x => x.Value == App.Settings.Prop.ChannelChangeMode).Key; - set => App.Settings.Prop.ChannelChangeMode = ChannelChangeModes[value]; - } } } From 054379d4f0faf57d1c144a1ee6a711fc37fc80c0 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 13 Jul 2023 13:15:03 +0100 Subject: [PATCH 083/111] Channel selection - editable textbox, error icon --- .../UI/Elements/Menu/Pages/BehaviourPage.xaml | 94 +++++++++---------- .../Elements/Menu/Pages/BehaviourPage.xaml.cs | 8 +- .../UI/ViewModels/Menu/BehaviourViewModel.cs | 36 ++++--- 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml index 61d4049..0c193c2 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml @@ -42,66 +42,58 @@ - - + - + + + + + + + + + - + - - - - - - - - - - - - - - - + + - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs index a1627f7..db864e1 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs @@ -20,14 +20,14 @@ namespace Bloxstrap.UI.Menu.Pages // https://stackoverflow.com/a/13289118/11852173 // yes this doesnt fully conform to xaml but whatever - private void TextBox_KeyEnterUpdate(object sender, KeyEventArgs e) + private void ComboBox_KeyEnterUpdate(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { - TextBox tBox = (TextBox)sender; - DependencyProperty prop = TextBox.TextProperty; + ComboBox box = (ComboBox)sender; + DependencyProperty prop = ComboBox.TextProperty; - BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop); + BindingExpression binding = BindingOperations.GetBindingExpression(box, prop); if (binding is not null) binding.UpdateSource(); diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs index 78661f3..6c19562 100644 --- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs @@ -26,10 +26,14 @@ namespace Bloxstrap.UI.ViewModels.Menu private async Task LoadChannelDeployInfo(string channel) { + LoadingSpinnerVisibility = Visibility.Visible; + LoadingErrorVisibility = Visibility.Collapsed; ChannelInfoLoadingText = "Fetching latest deploy info, please wait..."; - OnPropertyChanged(nameof(ChannelInfoLoadingText)); - ChannelDeployInfo = null; + + OnPropertyChanged(nameof(LoadingSpinnerVisibility)); + OnPropertyChanged(nameof(LoadingErrorVisibility)); + OnPropertyChanged(nameof(ChannelInfoLoadingText)); OnPropertyChanged(nameof(ChannelDeployInfo)); try @@ -47,11 +51,19 @@ namespace Bloxstrap.UI.ViewModels.Menu } catch (Exception) { - ChannelInfoLoadingText = "Failed to get deploy info.\nIs the channel name valid?"; + LoadingSpinnerVisibility = Visibility.Collapsed; + LoadingErrorVisibility = Visibility.Visible; + ChannelInfoLoadingText = "Could not get deployment information. Is the channel name valid?"; + + OnPropertyChanged(nameof(LoadingSpinnerVisibility)); + OnPropertyChanged(nameof(LoadingErrorVisibility)); OnPropertyChanged(nameof(ChannelInfoLoadingText)); } } + public Visibility LoadingSpinnerVisibility { get; private set; } = Visibility.Visible; + public Visibility LoadingErrorVisibility { get; private set; } = Visibility.Collapsed; + public DeployInfo? ChannelDeployInfo { get; private set; } = null; public string ChannelInfoLoadingText { get; private set; } = null!; @@ -69,12 +81,16 @@ namespace Bloxstrap.UI.ViewModels.Menu public IEnumerable Channels => RobloxDeployment.SelectableChannels; - public string Channel + public string SelectedChannel { get => App.Settings.Prop.Channel; set { value = value.Trim(); + + if (String.IsNullOrEmpty(value)) + value = RobloxDeployment.DefaultChannel; + Task.Run(() => LoadChannelDeployInfo(value)); App.Settings.Prop.Channel = value; } @@ -90,20 +106,14 @@ namespace Bloxstrap.UI.ViewModels.Menu if (!value) { // roblox typically sets channels in all lowercase, so here we find if a case insensitive match exists - string? matchingChannel = Channels.Where(x => x.ToLowerInvariant() == Channel.ToLowerInvariant()).FirstOrDefault(); - Channel = string.IsNullOrEmpty(matchingChannel) ? RobloxDeployment.DefaultChannel : matchingChannel; + string? matchingChannel = Channels.Where(x => x.ToLowerInvariant() == SelectedChannel.ToLowerInvariant()).FirstOrDefault(); + SelectedChannel = string.IsNullOrEmpty(matchingChannel) ? RobloxDeployment.DefaultChannel : matchingChannel; } - OnPropertyChanged(nameof(Channel)); - OnPropertyChanged(nameof(ChannelComboBoxVisibility)); - OnPropertyChanged(nameof(ChannelTextBoxVisibility)); + OnPropertyChanged(nameof(SelectedChannel)); } } - // cant use data bindings so i have to do whatever tf this is - public Visibility ChannelComboBoxVisibility => ManualChannelEntry ? Visibility.Collapsed : Visibility.Visible; - public Visibility ChannelTextBoxVisibility => ManualChannelEntry ? Visibility.Visible : Visibility.Collapsed; - // todo - move to enum attributes? public IReadOnlyDictionary ChannelChangeModes => new Dictionary { From e683af373d2407bf8ddc4255132c4a7bf2edae22 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 13 Jul 2023 13:29:27 +0100 Subject: [PATCH 084/111] Channel selector - warning if out of date --- Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml | 7 +++++++ Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml index 0c193c2..9f28d66 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml @@ -61,6 +61,7 @@ + @@ -75,6 +76,11 @@ + + + + + @@ -93,6 +99,7 @@ + diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs index 6c19562..331c997 100644 --- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs @@ -40,6 +40,11 @@ namespace Bloxstrap.UI.ViewModels.Menu { ClientVersion info = await RobloxDeployment.GetInfo(channel, true); + if (info.Timestamp?.AddMonths(1) < DateTime.Now) + ChannelWarningVisibility = Visibility.Visible; + else + ChannelWarningVisibility = Visibility.Collapsed; + ChannelDeployInfo = new DeployInfo { Version = info.Version, @@ -47,13 +52,14 @@ namespace Bloxstrap.UI.ViewModels.Menu Timestamp = info.Timestamp?.ToFriendlyString()! }; + OnPropertyChanged(nameof(ChannelWarningVisibility)); OnPropertyChanged(nameof(ChannelDeployInfo)); } catch (Exception) { LoadingSpinnerVisibility = Visibility.Collapsed; LoadingErrorVisibility = Visibility.Visible; - ChannelInfoLoadingText = "Could not get deployment information. Is the channel name valid?"; + ChannelInfoLoadingText = "Could not get deployment information! Is the channel name valid?"; OnPropertyChanged(nameof(LoadingSpinnerVisibility)); OnPropertyChanged(nameof(LoadingErrorVisibility)); @@ -63,6 +69,7 @@ namespace Bloxstrap.UI.ViewModels.Menu public Visibility LoadingSpinnerVisibility { get; private set; } = Visibility.Visible; public Visibility LoadingErrorVisibility { get; private set; } = Visibility.Collapsed; + public Visibility ChannelWarningVisibility { get; private set; } = Visibility.Collapsed; public DeployInfo? ChannelDeployInfo { get; private set; } = null; public string ChannelInfoLoadingText { get; private set; } = null!; From 88ea69c56d57d037ef23ab3eeca388d207a74652 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 14 Jul 2023 23:20:58 +0100 Subject: [PATCH 085/111] Rework emoji font mod --- Bloxstrap/Bootstrapper.cs | 21 ++++++++----------- Bloxstrap/Extensions/EmojiTypeEx.cs | 12 ++++++++++- Bloxstrap/Models/Settings.cs | 2 +- Bloxstrap/Models/State.cs | 1 - Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs | 4 ++-- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 9e83cbd..1f6e4d9 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1010,27 +1010,24 @@ namespace Bloxstrap await CheckModPreset(App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"), @"ExtraContent\places\Mobile.rbxl", ""); // emoji presets are downloaded remotely from github due to how large they are - string emojiFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\TwemojiMozilla.ttf"); + string contentFonts = Path.Combine(Directories.Modifications, "content\\fonts"); + string emojiFontLocation = Path.Combine(contentFonts, "TwemojiMozilla.ttf"); + string emojiFontHash = File.Exists(emojiFontLocation) ? Utility.MD5Hash.FromFile(emojiFontLocation) : ""; - if (App.Settings.Prop.PreferredEmojiType == EmojiType.Default && App.State.Prop.CurrentEmojiType != EmojiType.Default) + if (App.Settings.Prop.EmojiType == EmojiType.Default && EmojiTypeEx.Hashes.Values.Contains(emojiFontHash)) { - if (File.Exists(emojiFontLocation)) - File.Delete(emojiFontLocation); - - App.State.Prop.CurrentEmojiType = EmojiType.Default; + File.Delete(emojiFontLocation); } - else if (App.Settings.Prop.PreferredEmojiType != EmojiType.Default && App.State.Prop.CurrentEmojiType != App.Settings.Prop.PreferredEmojiType) + else if (App.Settings.Prop.EmojiType != EmojiType.Default && emojiFontHash != App.Settings.Prop.EmojiType.GetHash()) { - if (File.Exists(emojiFontLocation)) + if (emojiFontHash != "") File.Delete(emojiFontLocation); - string remoteEmojiLocation = App.Settings.Prop.PreferredEmojiType.GetRemoteLocation(); + Directory.CreateDirectory(contentFonts); - var response = await App.HttpClient.GetAsync(remoteEmojiLocation); + var response = await App.HttpClient.GetAsync(App.Settings.Prop.EmojiType.GetUrl()); await using var fileStream = new FileStream(emojiFontLocation, FileMode.CreateNew); await response.Content.CopyToAsync(fileStream); - - App.State.Prop.CurrentEmojiType = App.Settings.Prop.PreferredEmojiType; } foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) diff --git a/Bloxstrap/Extensions/EmojiTypeEx.cs b/Bloxstrap/Extensions/EmojiTypeEx.cs index 5978b66..07fd220 100644 --- a/Bloxstrap/Extensions/EmojiTypeEx.cs +++ b/Bloxstrap/Extensions/EmojiTypeEx.cs @@ -23,7 +23,17 @@ namespace Bloxstrap.Extensions { EmojiType.Windows8, "Win8.1SegoeUIEmoji.ttf" }, }; - public static string GetRemoteLocation(this EmojiType emojiType) + public static IReadOnlyDictionary Hashes => new Dictionary + { + { EmojiType.Catmoji, "98138f398a8cde897074dd2b8d53eca0" }, + { EmojiType.Windows11, "d50758427673578ddf6c9edcdbf367f5" }, + { EmojiType.Windows10, "d8a7eecbebf9dfdf622db8ccda63aff5" }, + { EmojiType.Windows8, "2b01c6caabbe95afc92aa63b9bf100f3" }, + }; + + public static string GetHash(this EmojiType emojiType) => Hashes[emojiType]; + + public static string GetUrl(this EmojiType emojiType) { if (emojiType == EmojiType.Default) return ""; diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index 477d324..80e28c2 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -31,7 +31,7 @@ namespace Bloxstrap.Models public bool UseOldCharacterSounds { get; set; } = false; public bool UseOldMouseCursor { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false; - public EmojiType PreferredEmojiType { get; set; } = EmojiType.Default; + public EmojiType EmojiType { get; set; } = EmojiType.Default; public bool DisableFullscreenOptimizations { get; set; } = false; } } diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index cd1a7fc..b099001 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -7,7 +7,6 @@ namespace Bloxstrap.Models public class State { public string VersionGuid { get; set; } = ""; - public EmojiType CurrentEmojiType { get; set; } = EmojiType.Default; public List ModManifest { get; set; } = new(); } } diff --git a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs index a1b20ca..1f94b4c 100644 --- a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs @@ -44,8 +44,8 @@ namespace Bloxstrap.UI.ViewModels.Menu public string SelectedEmojiType { - get => EmojiTypes.FirstOrDefault(x => x.Value == App.Settings.Prop.PreferredEmojiType).Key; - set => App.Settings.Prop.PreferredEmojiType = EmojiTypes[value]; + get => EmojiTypes.FirstOrDefault(x => x.Value == App.Settings.Prop.EmojiType).Key; + set => App.Settings.Prop.EmojiType = EmojiTypes[value]; } public bool DisableFullscreenOptimizationsEnabled From ade4a893388c968a4e7ea63b1ed0226d43c14cba Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 00:05:49 +0100 Subject: [PATCH 086/111] Add option to use pre-2013 mouse cursor --- Bloxstrap/Bloxstrap.csproj | 8 +- Bloxstrap/Bootstrapper.cs | 37 ++++--- Bloxstrap/Enums/CursorType.cs | 9 ++ Bloxstrap/Extensions/CursorTypeEx.cs | 16 +++ Bloxstrap/Models/Settings.cs | 2 +- .../Mods/Cursor/From2006/ArrowCursor.png | Bin 0 -> 2065 bytes .../Mods/Cursor/From2006/ArrowFarCursor.png | Bin 0 -> 2169 bytes .../From2013/ArrowCursor.png} | Bin .../From2013/ArrowFarCursor.png} | Bin .../UI/Elements/Menu/Pages/ModsPage.xaml | 103 ++++++++---------- Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs | 8 +- 11 files changed, 101 insertions(+), 82 deletions(-) create mode 100644 Bloxstrap/Enums/CursorType.cs create mode 100644 Bloxstrap/Extensions/CursorTypeEx.cs create mode 100644 Bloxstrap/Resources/Mods/Cursor/From2006/ArrowCursor.png create mode 100644 Bloxstrap/Resources/Mods/Cursor/From2006/ArrowFarCursor.png rename Bloxstrap/Resources/Mods/{OldCursor.png => Cursor/From2013/ArrowCursor.png} (100%) rename Bloxstrap/Resources/Mods/{OldFarCursor.png => Cursor/From2013/ArrowFarCursor.png} (100%) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 11901f0..f1ce5f1 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -24,10 +24,12 @@ + + + + - - diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 1f6e4d9..16059f8 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -998,15 +998,20 @@ namespace Bloxstrap if (!Directory.Exists(modFolder)) Directory.CreateDirectory(modFolder); - await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); - await CheckModPreset(App.Settings.Prop.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); + // cursors + await CheckModPreset(App.Settings.Prop.CursorType != CursorType.Default, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", $"Cursor.{App.Settings.Prop.CursorType}.ArrowCursor.png"); + await CheckModPreset(App.Settings.Prop.CursorType != CursorType.Default, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", $"Cursor.{App.Settings.Prop.CursorType}.ArrowFarCursor.png"); + + // character sounds await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_falling.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump_land.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_swim.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); + await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); + + // misc await CheckModPreset(App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"), @"ExtraContent\places\Mobile.rbxl", ""); // emoji presets are downloaded remotely from github due to how large they are @@ -1104,26 +1109,22 @@ namespace Bloxstrap private static async Task CheckModPreset(bool condition, string location, string name) { - string modFolderLocation = Path.Combine(Directories.Modifications, location); - byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); + string fullLocation = Path.Combine(Directories.Modifications, location); + byte[] embeddedData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); - if (condition) - { - if (!File.Exists(modFolderLocation)) - { - string? directory = Path.GetDirectoryName(modFolderLocation); + string fileHash = File.Exists(fullLocation) ? Utility.MD5Hash.FromFile(fullLocation) : ""; + string embeddedHash = Utility.MD5Hash.FromBytes(embeddedData); - if (directory is null) - return; + if (condition && fileHash != embeddedHash) + { + Directory.CreateDirectory(Path.GetDirectoryName(fullLocation)!); + File.Delete(fullLocation); - Directory.CreateDirectory(directory); - - await File.WriteAllBytesAsync(modFolderLocation, binaryData); - } + await File.WriteAllBytesAsync(fullLocation, embeddedData); } - else if (File.Exists(modFolderLocation) && Utility.MD5Hash.FromFile(modFolderLocation) == Utility.MD5Hash.FromBytes(binaryData)) + else if (!condition && fileHash != "") { - File.Delete(modFolderLocation); + File.Delete(fullLocation); } } diff --git a/Bloxstrap/Enums/CursorType.cs b/Bloxstrap/Enums/CursorType.cs new file mode 100644 index 0000000..98e8ec4 --- /dev/null +++ b/Bloxstrap/Enums/CursorType.cs @@ -0,0 +1,9 @@ +namespace Bloxstrap.Enums +{ + public enum CursorType + { + Default, + From2006, + From2013 + } +} diff --git a/Bloxstrap/Extensions/CursorTypeEx.cs b/Bloxstrap/Extensions/CursorTypeEx.cs new file mode 100644 index 0000000..3afe692 --- /dev/null +++ b/Bloxstrap/Extensions/CursorTypeEx.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +using Bloxstrap.Enums; + +namespace Bloxstrap.Extensions +{ + static class CursorTypeEx + { + public static IReadOnlyDictionary Selections => new Dictionary + { + { "Default", CursorType.Default }, + { "Before 2022", CursorType.From2013 }, + { "Before 2013", CursorType.From2006 }, + }; + } +} diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index 80e28c2..604e3c2 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -29,8 +29,8 @@ namespace Bloxstrap.Models // mod preset configuration public bool UseOldDeathSound { get; set; } = true; public bool UseOldCharacterSounds { get; set; } = false; - public bool UseOldMouseCursor { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false; + public CursorType CursorType { get; set; } = CursorType.Default; public EmojiType EmojiType { get; set; } = EmojiType.Default; public bool DisableFullscreenOptimizations { get; set; } = false; } diff --git a/Bloxstrap/Resources/Mods/Cursor/From2006/ArrowCursor.png b/Bloxstrap/Resources/Mods/Cursor/From2006/ArrowCursor.png new file mode 100644 index 0000000000000000000000000000000000000000..85c463983a935de602927cec0f490c630091476c GIT binary patch literal 2065 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 literal 0 HcmV?d00001 diff --git a/Bloxstrap/Resources/Mods/Cursor/From2006/ArrowFarCursor.png b/Bloxstrap/Resources/Mods/Cursor/From2006/ArrowFarCursor.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1954a334a7b330ad6beae16d11c14ef670f0a6 GIT binary patch literal 2169 zcmb7F`8yMiA9uDKi_8$JIaY3SPAILAOmY;V^wpp6eLv6Z{d%6)^Ss`#=Xw3|PDa^W0t!kCa&d71krt+Q2aNiM!-o!b z`q7Q*15zWIp-C4*ys;#F;0=faKERvH0T&9<)`r-72SK!9Fg=KtwxKS}PzMfChZ(?N zwQnsJ4}1U;(%K9#!Yw2r!{;Ec>Tw_m1X(x}xwuYr`~!Cv-ogGrSY16@LF+oaSgb4);Jk_6gkjP{!bg|pz%PGOkLPT40bMx1{hYxdyaHQFM zz}&blELQ+}mu^>8B1Kr{@Vm6|;A0*m&VHe0W@2)(vW`lhJZUoY=~HV>X=!)i5RNf1 zKE7go>Yj*KF%?A?201hM8NIJXv>*tL9d1x?x-Atp6Pu!>q9Qoa5HMkzm6f$n+xKvO zHY_a6XZwczs*!Nntr|k0IS^(1xMUrG8z#^vySg4vybSuY`?CU%$9E464K*eSAM12; zbyf3(DsL)MdlU3pp8LwGvP`9ZodpG_1x_(`FwV|nyztmxYct`E{$n9Z%E}ew$)@0K z27{qieYdZmtv#?EP7NX&(cz=)!k2-dUvOn?M;w1NtMclNv(0r)O=O~@ zfNEr9^bm~fOMEe6jsCL{E6DjJh)7&tE+p{lW}q|COW#Q zwY9bMzkA*%Fvb4MITFa=+Eo3^t5Tw(qFV+cXM$%{ni7| zElg{yiIQ93gD=4RAIdbeugLn@K04RGyu2JK4(LbMdnJ3~@n0z^DJcSUI(-rYlUo0I zHOK_bRp8kvwg;8%h`@kx(q}>xGg4Bd+4p30baZGNo11=+&nxruUSZC=a)KSYs5iU8 zJawkBXubw}d_A@+$qfpHo)c-o&h?~q%`7fZD3k|dIguRyAOn!edr-#%?IYbrA#-6V z?$*}UfnDJEmmoDwO}4_6ncVdr7Ml&w)7Q__%$hjCN9-VMiMx)BM`10Qi?=Ct`9w*Q zqVU-uZf(w0OF6I6TKgw9F%df(3uF8H`z=?tWVe6($d8`u0fzq=n6JE;d*@G1ZaM#> za@z(d|6Mvc{@Dde!5v>|^lZUF1C~A%Dpe}hYgEBP`<{xbYN(u?T=2&ywttTVd_pK~ z&?_rd*o%uk1pW%zW1iNeWIooCy^LRn)S41bSWs{lH6TxtIl>vK{#g9VFO*2sA?W7C za*XaKXJoirz*i?416M|>uT5l=Ed&+XrrwzP$IPN}s*tBUvMWL1;Y*e|8Vi2xntoRX zIan{oQQY&ywtbOrN^0snacO{tfJ=hG5nyrgwWA8=RB!yG*~h#JUbEyEy*nkcgzo9$ z=no}GS*-#8&=TFtV!EM4yEXhFW%uukr>ExEhku_MD9NQ#4GiVwI}4Sv_6vMG(hVS@ z{igb>4tcZn!|y(1-(s8;t+?=(F|0vSoQa{XO zq6%m-o+^Ax)LjH@#Zf*->$5lSE0-lI*#&_eH!r?RwR%Eg(QQe*J*)`HYZq#4%zP zbc&nUE=LG#c*d03E*^+aDy6mR_KSnW#3~)D&_x`TCeJtN=6$OxZB?&wetKH20Q0F9 zV%dcP)sW*N`3O7+tDJDcr6p!>D(n{_#G!+1H~NF)c5Et(k+c0cLCnoj=8yXtm91iEJ!q0h-!Jdv83NL%AI!Fotxi?28#5ZuS>pR=Ai z4cb~+F{+ztkXbu|CA`&?mDX*_cn8fY;_scVz1M4NVVhBUCB`A!B&PkhZWq-r z*)yIP0Pw_p`8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs index 1f94b4c..86cac7e 100644 --- a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs @@ -28,10 +28,12 @@ namespace Bloxstrap.UI.ViewModels.Menu set => App.Settings.Prop.UseOldCharacterSounds = value; } - public bool OldMouseCursorEnabled + public IReadOnlyDictionary CursorTypes => CursorTypeEx.Selections; + + public string SelectedCursorType { - get => App.Settings.Prop.UseOldMouseCursor; - set => App.Settings.Prop.UseOldMouseCursor = value; + get => CursorTypes.FirstOrDefault(x => x.Value == App.Settings.Prop.CursorType).Key; + set => App.Settings.Prop.CursorType = CursorTypes[value]; } public bool DisableAppPatchEnabled From f3b7bbbbc6bf5674a73f1381c5b4df9e8b2950a6 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 00:07:36 +0100 Subject: [PATCH 087/111] turns out its 2008 and not 2009... --- Bloxstrap/Enums/BootstrapperIcon.cs | 2 +- Bloxstrap/Enums/BootstrapperStyle.cs | 2 +- Bloxstrap/Extensions/BootstrapperIconEx.cs | 2 +- Bloxstrap/Properties/Resources.Designer.cs | 4 ++-- Bloxstrap/UI/Controls.cs | 2 +- ...og2009.Designer.cs => LegacyDialog2008.Designer.cs} | 10 +++++----- .../{LegacyDialog2009.cs => LegacyDialog2008.cs} | 6 +++--- .../{LegacyDialog2009.resx => LegacyDialog2008.resx} | 0 Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs | 6 +++--- 9 files changed, 17 insertions(+), 17 deletions(-) rename Bloxstrap/UI/Elements/Bootstrapper/{LegacyDialog2009.Designer.cs => LegacyDialog2008.Designer.cs} (95%) rename Bloxstrap/UI/Elements/Bootstrapper/{LegacyDialog2009.cs => LegacyDialog2008.cs} (86%) rename Bloxstrap/UI/Elements/Bootstrapper/{LegacyDialog2009.resx => LegacyDialog2008.resx} (100%) diff --git a/Bloxstrap/Enums/BootstrapperIcon.cs b/Bloxstrap/Enums/BootstrapperIcon.cs index 4a44b87..a67185d 100644 --- a/Bloxstrap/Enums/BootstrapperIcon.cs +++ b/Bloxstrap/Enums/BootstrapperIcon.cs @@ -3,7 +3,7 @@ public enum BootstrapperIcon { IconBloxstrap, - Icon2009, + Icon2008, Icon2011, IconEarly2015, IconLate2015, diff --git a/Bloxstrap/Enums/BootstrapperStyle.cs b/Bloxstrap/Enums/BootstrapperStyle.cs index cade7cb..2d1b063 100644 --- a/Bloxstrap/Enums/BootstrapperStyle.cs +++ b/Bloxstrap/Enums/BootstrapperStyle.cs @@ -3,7 +3,7 @@ public enum BootstrapperStyle { VistaDialog, - LegacyDialog2009, + LegacyDialog2008, LegacyDialog2011, ProgressDialog, FluentDialog, diff --git a/Bloxstrap/Extensions/BootstrapperIconEx.cs b/Bloxstrap/Extensions/BootstrapperIconEx.cs index 5b8721b..dad9cef 100644 --- a/Bloxstrap/Extensions/BootstrapperIconEx.cs +++ b/Bloxstrap/Extensions/BootstrapperIconEx.cs @@ -33,7 +33,7 @@ namespace Bloxstrap.Extensions return icon switch { BootstrapperIcon.IconBloxstrap => Properties.Resources.IconBloxstrap, - BootstrapperIcon.Icon2009 => Properties.Resources.Icon2009, + BootstrapperIcon.Icon2008 => Properties.Resources.Icon2008, BootstrapperIcon.Icon2011 => Properties.Resources.Icon2011, BootstrapperIcon.IconEarly2015 => Properties.Resources.IconEarly2015, BootstrapperIcon.IconLate2015 => Properties.Resources.IconLate2015, diff --git a/Bloxstrap/Properties/Resources.Designer.cs b/Bloxstrap/Properties/Resources.Designer.cs index 2e36579..a65c8d7 100644 --- a/Bloxstrap/Properties/Resources.Designer.cs +++ b/Bloxstrap/Properties/Resources.Designer.cs @@ -115,11 +115,11 @@ namespace Bloxstrap.Properties { /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Icon2009 + internal static System.Drawing.Icon Icon2008 { get { - object obj = ResourceManager.GetObject("Icon2009", resourceCulture); + object obj = ResourceManager.GetObject("Icon2008", resourceCulture); return ((System.Drawing.Icon)(obj)); } } diff --git a/Bloxstrap/UI/Controls.cs b/Bloxstrap/UI/Controls.cs index 22a6a61..34115be 100644 --- a/Bloxstrap/UI/Controls.cs +++ b/Bloxstrap/UI/Controls.cs @@ -47,7 +47,7 @@ namespace Bloxstrap.UI return style switch { BootstrapperStyle.VistaDialog => new VistaDialog(), - BootstrapperStyle.LegacyDialog2009 => new LegacyDialog2009(), + BootstrapperStyle.LegacyDialog2008 => new LegacyDialog2008(), BootstrapperStyle.LegacyDialog2011 => new LegacyDialog2011(), BootstrapperStyle.ProgressDialog => new ProgressDialog(), BootstrapperStyle.FluentDialog => new FluentDialog(), diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.Designer.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.Designer.cs similarity index 95% rename from Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.Designer.cs rename to Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.Designer.cs index d518694..2da060a 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.Designer.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.Designer.cs @@ -2,7 +2,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper { - partial class LegacyDialog2009 + partial class LegacyDialog2008 { /// /// Required designer variable. @@ -65,7 +65,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper this.buttonCancel.UseVisualStyleBackColor = true; this.buttonCancel.Click += new System.EventHandler(this.ButtonCancel_Click); // - // LegacyDialog2009 + // LegacyDialog2008 // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; @@ -79,10 +79,10 @@ namespace Bloxstrap.UI.Elements.Bootstrapper this.MaximumSize = new System.Drawing.Size(327, 161); this.MinimizeBox = false; this.MinimumSize = new System.Drawing.Size(327, 161); - this.Name = "LegacyDialog2009"; + this.Name = "LegacyDialog2008"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "LegacyDialog2009"; - this.Load += new System.EventHandler(this.LegacyDialog2009_Load); + this.Text = "LegacyDialog2008"; + this.Load += new System.EventHandler(this.LegacyDialog2008_Load); this.ResumeLayout(false); } diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs similarity index 86% rename from Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.cs rename to Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs index f66fdfa..a84042c 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs @@ -8,7 +8,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper // windows: https://youtu.be/VpduiruysuM?t=18 // mac: https://youtu.be/ncHhbcVDRgQ?t=63 - public partial class LegacyDialog2009 : WinFormsDialogBase + public partial class LegacyDialog2008 : WinFormsDialogBase { protected override string _message { @@ -34,7 +34,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper set => this.buttonCancel.Enabled = value; } - public LegacyDialog2009() + public LegacyDialog2008() { InitializeComponent(); @@ -42,7 +42,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper SetupDialog(); } - private void LegacyDialog2009_Load(object sender, EventArgs e) + private void LegacyDialog2008_Load(object sender, EventArgs e) { this.Activate(); } diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.resx b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.resx similarity index 100% rename from Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2009.resx rename to Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.resx diff --git a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs index 489c8fd..4e5e6f1 100644 --- a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs @@ -77,8 +77,8 @@ namespace Bloxstrap.UI.ViewModels.Menu { "Fluent", BootstrapperStyle.FluentDialog }, { "Progress (~2014)", BootstrapperStyle.ProgressDialog }, { "Legacy (2011 - 2014)", BootstrapperStyle.LegacyDialog2011 }, - { "Legacy (2009 - 2011)", BootstrapperStyle.LegacyDialog2009 }, - { "Vista (2009 - 2011)", BootstrapperStyle.VistaDialog }, + { "Legacy (2008 - 2011)", BootstrapperStyle.LegacyDialog2008 }, + { "Vista (2008 - 2011)", BootstrapperStyle.VistaDialog }, { "Fake Byfron (2023)", BootstrapperStyle.ByfronDialog }, }; @@ -97,7 +97,7 @@ namespace Bloxstrap.UI.ViewModels.Menu { "Late 2015", BootstrapperIcon.IconLate2015 }, { "Early 2015", BootstrapperIcon.IconEarly2015 }, { "2011", BootstrapperIcon.Icon2011 }, - { "2009", BootstrapperIcon.Icon2009 }, + { "2008", BootstrapperIcon.Icon2008 }, { "Custom", BootstrapperIcon.IconCustom }, }; From 258746f5fa3ac67ba87b0709620d4984f70839c6 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 08:36:00 +0100 Subject: [PATCH 088/111] oops forgot this one --- Bloxstrap/Properties/Resources.resx | 4 ++-- Bloxstrap/Resources/{Icon2009.ico => Icon2008.ico} | Bin 2 files changed, 2 insertions(+), 2 deletions(-) rename Bloxstrap/Resources/{Icon2009.ico => Icon2008.ico} (100%) diff --git a/Bloxstrap/Properties/Resources.resx b/Bloxstrap/Properties/Resources.resx index af22cd5..7c24a62 100644 --- a/Bloxstrap/Properties/Resources.resx +++ b/Bloxstrap/Properties/Resources.resx @@ -130,8 +130,8 @@ ..\Resources\DarkCancelButtonHover.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - ..\Resources\Icon2009.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\Icon2008.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\Icon2011.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/Bloxstrap/Resources/Icon2009.ico b/Bloxstrap/Resources/Icon2008.ico similarity index 100% rename from Bloxstrap/Resources/Icon2009.ico rename to Bloxstrap/Resources/Icon2008.ico From d8842cc0ccaf16b30a1a6edc507d9a2c672300b0 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 10:53:17 +0100 Subject: [PATCH 089/111] Add debug flags, fix mod preset bug --- Bloxstrap/Bootstrapper.cs | 18 ++++--- Bloxstrap/Extensions/CursorTypeEx.cs | 4 +- Bloxstrap/Models/Settings.cs | 1 + .../UI/Elements/Menu/Pages/FastFlagsPage.xaml | 31 ++++++++++++ .../UI/ViewModels/Menu/FastFlagsViewModel.cs | 47 +++++++++++++++++-- 5 files changed, 88 insertions(+), 13 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 16059f8..98e2139 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1110,22 +1110,26 @@ namespace Bloxstrap private static async Task CheckModPreset(bool condition, string location, string name) { string fullLocation = Path.Combine(Directories.Modifications, location); - byte[] embeddedData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); - string fileHash = File.Exists(fullLocation) ? Utility.MD5Hash.FromFile(fullLocation) : ""; + + if (!condition) + { + if (fileHash != "") + File.Delete(fullLocation); + + return; + } + + byte[] embeddedData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); string embeddedHash = Utility.MD5Hash.FromBytes(embeddedData); - if (condition && fileHash != embeddedHash) + if (fileHash != embeddedHash) { Directory.CreateDirectory(Path.GetDirectoryName(fullLocation)!); File.Delete(fullLocation); await File.WriteAllBytesAsync(fullLocation, embeddedData); } - else if (!condition && fileHash != "") - { - File.Delete(fullLocation); - } } private async Task DownloadPackage(Package package) diff --git a/Bloxstrap/Extensions/CursorTypeEx.cs b/Bloxstrap/Extensions/CursorTypeEx.cs index 3afe692..d2ba437 100644 --- a/Bloxstrap/Extensions/CursorTypeEx.cs +++ b/Bloxstrap/Extensions/CursorTypeEx.cs @@ -9,8 +9,8 @@ namespace Bloxstrap.Extensions public static IReadOnlyDictionary Selections => new Dictionary { { "Default", CursorType.Default }, - { "Before 2022", CursorType.From2013 }, - { "Before 2013", CursorType.From2006 }, + { "2013 - 2022", CursorType.From2013 }, + { "2006 - 2013", CursorType.From2006 }, }; } } diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index 604e3c2..104509f 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -15,6 +15,7 @@ namespace Bloxstrap.Models public bool CheckForUpdates { get; set; } = true; public bool CreateDesktopIcon { get; set; } = true; public bool MultiInstanceLaunching { get; set; } = false; + public bool OhHeyYouFoundMe { get; set; } = false; // channel configuration public string Channel { get; set; } = RobloxDeployment.DefaultChannel; diff --git a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml index 5fa2d85..5a49eaf 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml @@ -41,6 +41,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs index 566324c..f1a1706 100644 --- a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs @@ -1,6 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Linq; +using System.Windows; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; @@ -16,6 +19,42 @@ namespace Bloxstrap.UI.ViewModels.Menu private void OpenClientSettings() => Utilities.ShellExecute(Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json")); + public Visibility ShowDebugFlags => App.Settings.Prop.OhHeyYouFoundMe ? Visibility.Visible : Visibility.Collapsed; + + public bool HttpRequestLogging + { + get => App.FastFlags.GetValue("DFLogHttpTraceLight") is not null; + set => App.FastFlags.SetValue("DFLogHttpTraceLight", value ? 12 : null); + } + + public string HttpRequestProxy + { + get => App.FastFlags.GetValue("DFStringDebugPlayerHttpProxyUrl") ?? ""; + + set + { + bool? boolValue = null; + string? stringValue = null; + + if (!String.IsNullOrEmpty(value)) + { + boolValue = true; + stringValue = value; + } + + App.FastFlags.SetValue("DFFlagDebugEnableHttpProxy", boolValue); + App.FastFlags.SetValue("DFStringDebugPlayerHttpProxyUrl", stringValue); + App.FastFlags.SetValue("DFStringHttpCurlProxyHostAndPort", stringValue); + App.FastFlags.SetValue("DFStringHttpCurlProxyHostAndPortForExternalUrl", stringValue); + } + } + + public string StateOverlayFlags + { + get => App.FastFlags.GetValue("FStringDebugShowFlagState") ?? ""; + set => App.FastFlags.SetValue("FStringDebugShowFlagState", String.IsNullOrEmpty(value) ? null : value); + } + public int FramerateLimit { get => int.TryParse(App.FastFlags.GetValue("DFIntTaskSchedulerTargetFps"), out int x) ? x : 60; @@ -111,18 +150,18 @@ namespace Bloxstrap.UI.ViewModels.Menu return mode.Key; } - return "Automatic"; + return LightingTechnologies.First().Key; } set { foreach (var mode in LightingTechnologies) { - if (mode.Key != "Automatic") + if (mode.Key != LightingTechnologies.First().Key) App.FastFlags.SetValue(mode.Value, null); } - if (value != "Automatic") + if (value != LightingTechnologies.First().Key) App.FastFlags.SetValue(LightingTechnologies[value], "True"); } } From 41ca47ad0b3f587bbceda5ae56f34429c7b2a013 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 11:01:14 +0100 Subject: [PATCH 090/111] Add button to reset chosen install location --- Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml | 4 +++- Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml index b478e7b..7276410 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml @@ -23,9 +23,11 @@ + - + + diff --git a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs index 5a659f7..6ad864b 100644 --- a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs @@ -11,7 +11,10 @@ namespace Bloxstrap.UI.ViewModels.Menu public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + private string _originalInstallLocation = App.BaseDirectory; + public ICommand BrowseInstallLocationCommand => new RelayCommand(BrowseInstallLocation); + public ICommand ResetInstallLocationCommand => new RelayCommand(ResetInstallLocation); public ICommand OpenFolderCommand => new RelayCommand(OpenFolder); private void BrowseInstallLocation() @@ -22,7 +25,12 @@ namespace Bloxstrap.UI.ViewModels.Menu return; InstallLocation = dialog.SelectedPath; + OnPropertyChanged(nameof(InstallLocation)); + } + private void ResetInstallLocation() + { + InstallLocation = _originalInstallLocation; OnPropertyChanged(nameof(InstallLocation)); } From 63ef9072468342a3c774ba6da834a480c243b880 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 12:47:27 +0100 Subject: [PATCH 091/111] Add bulk font replacement, Win32 for file picker --- Bloxstrap/Bootstrapper.cs | 41 ++++++++++++++++- Bloxstrap/Models/FontFace.cs | 20 ++++++++ Bloxstrap/Models/FontFamily.cs | 14 ++++++ .../Elements/Menu/Pages/InstallationPage.xaml | 2 +- .../UI/Elements/Menu/Pages/ModsPage.xaml | 16 ++++++- .../UI/ViewModels/Menu/AppearanceViewModel.cs | 18 ++++---- Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs | 46 +++++++++++++++++-- 7 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 Bloxstrap/Models/FontFace.cs create mode 100644 Bloxstrap/Models/FontFamily.cs diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 98e2139..cc64143 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; @@ -115,7 +116,7 @@ namespace Bloxstrap if (Dialog is not null) Dialog.ProgressValue = newProgress; } - + public async Task Run() { App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); @@ -1035,6 +1036,44 @@ namespace Bloxstrap await response.Content.CopyToAsync(fileStream); } + // check custom font mod + // instead of replacing the fonts themselves, we'll just alter the font family manifests + + string modFontFamiliesFolder = Path.Combine(Directories.Modifications, "content\\fonts\\families"); + string customFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\CustomFont.ttf"); + + if (File.Exists(customFontLocation)) + { + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Begin font check"); + + Directory.CreateDirectory(modFontFamiliesFolder); + + foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families"))) + { + string jsonFilename = Path.GetFileName(jsonFilePath); + string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); + + if (File.Exists(modFilepath)) + continue; + + FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); + + if (fontFamilyData is null) + continue; + + foreach (FontFace fontFace in fontFamilyData.Faces) + fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; + + File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); + } + + App.Logger.WriteLine("[Bootstrapper::ApplyModifications] End font check"); + } + else + { + Directory.Delete(modFontFamiliesFolder, true); + } + foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) { // get relative directory path diff --git a/Bloxstrap/Models/FontFace.cs b/Bloxstrap/Models/FontFace.cs new file mode 100644 index 0000000..50bb179 --- /dev/null +++ b/Bloxstrap/Models/FontFace.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Bloxstrap.Models +{ + public class FontFace + { + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("weight")] + public int Weight { get; set; } + + [JsonPropertyName("style")] + public string Style { get; set; } = null!; + + [JsonPropertyName("assetId")] + public string AssetId { get; set; } = null!; + } +} diff --git a/Bloxstrap/Models/FontFamily.cs b/Bloxstrap/Models/FontFamily.cs new file mode 100644 index 0000000..9a846e1 --- /dev/null +++ b/Bloxstrap/Models/FontFamily.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Bloxstrap.Models +{ + public class FontFamily + { + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("faces")] + public IEnumerable Faces { get; set; } = null!; + } +} diff --git a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml index 7276410..2790bc2 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml @@ -27,7 +27,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml index 9e3e57e..c69873d 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml @@ -113,6 +113,20 @@ + + + + + + Forces every in-game font to be a font that you choose. + + + + + + + + @@ -122,7 +136,7 @@ - + diff --git a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs index 4e5e6f1..c22ba1f 100644 --- a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; -using System.Windows.Forms; +using Microsoft.Win32; using System.Windows.Input; using System.Windows.Media; @@ -40,14 +40,16 @@ namespace Bloxstrap.UI.ViewModels.Menu private void BrowseCustomIconLocation() { - using var dialog = new OpenFileDialog(); - dialog.Filter = "Icon files (*.ico)|*.ico|All files (*.*)|*.*"; - - if (dialog.ShowDialog() == DialogResult.OK) + var dialog = new OpenFileDialog { - CustomIconLocation = dialog.FileName; - OnPropertyChanged(nameof(CustomIconLocation)); - } + Filter = "Icon files|*.ico|All files|*.*" + }; + + if (dialog.ShowDialog() != true) + return; + + CustomIconLocation = dialog.FileName; + OnPropertyChanged(nameof(CustomIconLocation)); } public AppearanceViewModel(Page page) diff --git a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs index 86cac7e..b02209f 100644 --- a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs @@ -1,21 +1,56 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Windows; using System.Windows.Input; using Bloxstrap.Enums; using Bloxstrap.Extensions; +using Microsoft.Win32; + using CommunityToolkit.Mvvm.Input; namespace Bloxstrap.UI.ViewModels.Menu { - public class ModsViewModel + public class ModsViewModel : INotifyPropertyChanged { - public ICommand OpenModsFolderCommand => new RelayCommand(OpenModsFolder); + public event PropertyChangedEventHandler? PropertyChanged; + public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); private void OpenModsFolder() => Process.Start("explorer.exe", Directories.Modifications); + private string _customFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\CustomFont.ttf"); + private bool _usingCustomFont => File.Exists(_customFontLocation); + + private void ManageCustomFont() + { + if (_usingCustomFont) + { + File.Delete(_customFontLocation); + } + else + { + var dialog = new OpenFileDialog + { + Filter = "Font files|*.ttf;*.otf|All files|*.*" + }; + + if (dialog.ShowDialog() != true) + return; + + Directory.CreateDirectory(Path.GetDirectoryName(_customFontLocation)!); + File.Copy(dialog.FileName, _customFontLocation); + } + + OnPropertyChanged(nameof(ChooseCustomFontVisibility)); + OnPropertyChanged(nameof(DeleteCustomFontVisibility)); + } + + public ICommand OpenModsFolderCommand => new RelayCommand(OpenModsFolder); + public bool OldDeathSoundEnabled { get => App.Settings.Prop.UseOldDeathSound; @@ -50,7 +85,12 @@ namespace Bloxstrap.UI.ViewModels.Menu set => App.Settings.Prop.EmojiType = EmojiTypes[value]; } - public bool DisableFullscreenOptimizationsEnabled + public Visibility ChooseCustomFontVisibility => _usingCustomFont ? Visibility.Collapsed : Visibility.Visible; + public Visibility DeleteCustomFontVisibility => _usingCustomFont ? Visibility.Visible : Visibility.Collapsed; + + public ICommand ManageCustomFontCommand => new RelayCommand(ManageCustomFont); + + public bool DisableFullscreenOptimizations { get => App.Settings.Prop.DisableFullscreenOptimizations; set => App.Settings.Prop.DisableFullscreenOptimizations = value; From 78869e5e7e21f1633ac905de239807caeecbd5e5 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 12:59:18 +0100 Subject: [PATCH 092/111] Default to LIVE if no launch channel set also fixed a bug with the font mod --- Bloxstrap/Bootstrapper.cs | 2 +- Bloxstrap/ProtocolHandler.cs | 51 ++++++++++++------- .../UI/Elements/Menu/Pages/BehaviourPage.xaml | 4 +- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index cc64143..8f6c975 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1069,7 +1069,7 @@ namespace Bloxstrap App.Logger.WriteLine("[Bootstrapper::ApplyModifications] End font check"); } - else + else if (Directory.Exists(modFontFamiliesFolder)) { Directory.Delete(modFontFamiliesFolder, true); } diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/ProtocolHandler.cs index ce1a4ac..4d4c58c 100644 --- a/Bloxstrap/ProtocolHandler.cs +++ b/Bloxstrap/ProtocolHandler.cs @@ -32,6 +32,8 @@ namespace Bloxstrap string[] keyvalPair; string key; string val; + bool channelArgPresent = false; + StringBuilder commandLine = new(); foreach (var parameter in protocol.Split('+')) @@ -56,25 +58,10 @@ namespace Bloxstrap if (key == "launchtime") val = "LAUNCHTIMEPLACEHOLDER"; - if (key == "channel") + if (key == "channel" && !String.IsNullOrEmpty(val)) { - if (val.ToLowerInvariant() != App.Settings.Prop.Channel.ToLowerInvariant() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) - { - MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic - ? MessageBoxResult.Yes - : Controls.ShowMessageBox( - $"Roblox is attempting to set your channel to {val}, however your current preferred channel is {App.Settings.Prop.Channel}.\n\n" + - $"Would you like to switch channels from {App.Settings.Prop.Channel} to {val}?", - MessageBoxImage.Question, - MessageBoxButton.YesNo - ); - - if (result == MessageBoxResult.Yes) - { - App.Logger.WriteLine($"[Protocol::ParseUri] Changed Roblox build channel from {App.Settings.Prop.Channel} to {val}"); - App.Settings.Prop.Channel = val; - } - } + channelArgPresent = true; + ChangeChannel(val); // we'll set the arg when launching continue; @@ -83,9 +70,37 @@ namespace Bloxstrap commandLine.Append(UriKeyArgMap[key] + val + " "); } + if (!channelArgPresent) + ChangeChannel(RobloxDeployment.DefaultChannel); + return commandLine.ToString(); } + public static void ChangeChannel(string channel) + { + if (channel.ToLowerInvariant() == App.Settings.Prop.Channel.ToLowerInvariant()) + return; + + if (App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Ignore) + return; + + if (App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Automatic) + { + MessageBoxResult result = Controls.ShowMessageBox( + $"Roblox is attempting to set your channel to {channel}, however your current preferred channel is {App.Settings.Prop.Channel}.\n\n" + + $"Would you like to switch channels from {App.Settings.Prop.Channel} to {channel}?", + MessageBoxImage.Question, + MessageBoxButton.YesNo + ); + + if (result != MessageBoxResult.Yes) + return; + } + + App.Logger.WriteLine($"[Protocol::ParseUri] Changed Roblox build channel from {App.Settings.Prop.Channel} to {channel}"); + App.Settings.Prop.Channel = channel; + } + public static void Register(string key, string name, string handler) { string handlerArgs = $"\"{handler}\" %1"; diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml index 9f28d66..9d118b2 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml @@ -39,8 +39,8 @@ - - + + From 232cd4f5c0bf26acd7d0221550a3e2ce07bd1d43 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 13:12:46 +0100 Subject: [PATCH 093/111] Fix bug with mod preset checking... again --- Bloxstrap/Bootstrapper.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 8f6c975..59e7714 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1000,9 +1000,11 @@ namespace Bloxstrap Directory.CreateDirectory(modFolder); // cursors - await CheckModPreset(App.Settings.Prop.CursorType != CursorType.Default, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", $"Cursor.{App.Settings.Prop.CursorType}.ArrowCursor.png"); - await CheckModPreset(App.Settings.Prop.CursorType != CursorType.Default, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", $"Cursor.{App.Settings.Prop.CursorType}.ArrowFarCursor.png"); - + await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2006, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2006.ArrowCursor.png"); + await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2006, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2006.ArrowFarCursor.png"); + await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2013, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2013.ArrowCursor.png"); + await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2013, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2013.ArrowFarCursor.png"); + // character sounds await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_footsteps_plastic.mp3", "OldWalk.mp3"); await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\action_jump.mp3", "OldJump.mp3"); @@ -1151,17 +1153,17 @@ namespace Bloxstrap string fullLocation = Path.Combine(Directories.Modifications, location); string fileHash = File.Exists(fullLocation) ? Utility.MD5Hash.FromFile(fullLocation) : ""; + byte[] embeddedData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); + string embeddedHash = Utility.MD5Hash.FromBytes(embeddedData); + if (!condition) { - if (fileHash != "") + if (fileHash != "" && fileHash == embeddedHash) File.Delete(fullLocation); return; } - byte[] embeddedData = string.IsNullOrEmpty(name) ? Array.Empty() : await Resource.Get(name); - string embeddedHash = Utility.MD5Hash.FromBytes(embeddedData); - if (fileHash != embeddedHash) { Directory.CreateDirectory(Path.GetDirectoryName(fullLocation)!); From e3cf2ac3a2f07ac5d85e865d0f70f829bbfce798 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 13:18:39 +0100 Subject: [PATCH 094/111] Minor visual adjustments --- Bloxstrap/Extensions/CursorTypeEx.cs | 4 ++-- Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/Extensions/CursorTypeEx.cs b/Bloxstrap/Extensions/CursorTypeEx.cs index d2ba437..a46dc7e 100644 --- a/Bloxstrap/Extensions/CursorTypeEx.cs +++ b/Bloxstrap/Extensions/CursorTypeEx.cs @@ -9,8 +9,8 @@ namespace Bloxstrap.Extensions public static IReadOnlyDictionary Selections => new Dictionary { { "Default", CursorType.Default }, - { "2013 - 2022", CursorType.From2013 }, - { "2006 - 2013", CursorType.From2006 }, + { "2013 (Angular)", CursorType.From2013 }, + { "2006 (Cartoony)", CursorType.From2006 }, }; } } diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml index c69873d..8972f0f 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml @@ -123,7 +123,7 @@ - + From e4dc6c0600a1c7f5c393401c758576d10cbd0e71 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 16:20:28 +0100 Subject: [PATCH 095/111] Show inner exception in error dialog --- Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs b/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs index 1af61aa..2b71bcb 100644 --- a/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs @@ -18,11 +18,16 @@ namespace Bloxstrap.UI { public ExceptionDialog(Exception exception) { + Exception? innerException = exception.InnerException; + InitializeComponent(); Title = RootTitleBar.Title = $"{App.ProjectName} Exception"; ErrorRichTextBox.Selection.Text = $"{exception.GetType()}: {exception.Message}"; + if (innerException is not null) + ErrorRichTextBox.Selection.Text += $"\n\n===== Inner Exception =====\n{innerException.GetType()}: {innerException.Message}"; + if (!App.Logger.Initialized) LocateLogFileButton.Content = "Copy log contents"; From f553a812cbc84dcf8f03f04b8355becea5f6f357 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 17:33:12 +0100 Subject: [PATCH 096/111] Add ability to set old avatar editor scene (#353) --- Bloxstrap/Bloxstrap.csproj | 1 + Bloxstrap/Bootstrapper.cs | 7 ++++-- Bloxstrap/Models/Settings.cs | 1 + .../Resources/Mods/OldAvatarBackground.rbxl | Bin 0 -> 161011 bytes .../UI/Elements/Menu/Pages/ModsPage.xaml | 22 +++++++++++++----- Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs | 6 +++++ 6 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 Bloxstrap/Resources/Mods/OldAvatarBackground.rbxl diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index f1ce5f1..5bd23b2 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -29,6 +29,7 @@ + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 59e7714..f9f263f 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -999,6 +999,8 @@ namespace Bloxstrap if (!Directory.Exists(modFolder)) Directory.CreateDirectory(modFolder); + bool appDisabled = App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"); + // cursors await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2006, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2006.ArrowCursor.png"); await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2006, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "Cursor.From2006.ArrowFarCursor.png"); @@ -1014,8 +1016,9 @@ namespace Bloxstrap await CheckModPreset(App.Settings.Prop.UseOldCharacterSounds, @"content\sounds\impact_water.mp3", "Empty.mp3"); await CheckModPreset(App.Settings.Prop.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); - // misc - await CheckModPreset(App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"), @"ExtraContent\places\Mobile.rbxl", ""); + // Mobile.rbxl + await CheckModPreset(appDisabled, @"ExtraContent\places\Mobile.rbxl", ""); + await CheckModPreset(App.Settings.Prop.UseOldAvatarBackground && !appDisabled, @"ExtraContent\places\Mobile.rbxl", "OldAvatarBackground.rbxl"); // emoji presets are downloaded remotely from github due to how large they are string contentFonts = Path.Combine(Directories.Modifications, "content\\fonts"); diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index 104509f..95fd18f 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -31,6 +31,7 @@ namespace Bloxstrap.Models public bool UseOldDeathSound { get; set; } = true; public bool UseOldCharacterSounds { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false; + public bool UseOldAvatarBackground { get; set; } = false; public CursorType CursorType { get; set; } = CursorType.Default; public EmojiType EmojiType { get; set; } = EmojiType.Default; public bool DisableFullscreenOptimizations { get; set; } = false; diff --git a/Bloxstrap/Resources/Mods/OldAvatarBackground.rbxl b/Bloxstrap/Resources/Mods/OldAvatarBackground.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..578e822d42457e69a7621f106c2930bd1b047333 GIT binary patch literal 161011 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 - + @@ -71,17 +71,27 @@ - + - + - + + + + + + + + + + + @@ -91,7 +101,7 @@ - + @@ -101,7 +111,7 @@ - + diff --git a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs index b02209f..7859f4a 100644 --- a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs @@ -71,6 +71,12 @@ namespace Bloxstrap.UI.ViewModels.Menu set => App.Settings.Prop.CursorType = CursorTypes[value]; } + public bool OldAvatarBackground + { + get => App.Settings.Prop.UseOldAvatarBackground; + set => App.Settings.Prop.UseOldAvatarBackground = value; + } + public bool DisableAppPatchEnabled { get => App.Settings.Prop.UseDisableAppPatch; From 0162dbf3bb88a5fd43ae4439bb2ba51ccb34c940 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 17:52:59 +0100 Subject: [PATCH 097/111] Make channel selection input more responsive --- .../UI/Elements/Menu/Pages/BehaviourPage.xaml | 2 +- .../UI/Elements/Menu/Pages/BehaviourPage.xaml.cs | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml index 9d118b2..50c85ca 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml @@ -42,7 +42,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs index db864e1..736197b 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs @@ -17,21 +17,5 @@ namespace Bloxstrap.UI.Menu.Pages DataContext = new BehaviourViewModel(); InitializeComponent(); } - - // https://stackoverflow.com/a/13289118/11852173 - // yes this doesnt fully conform to xaml but whatever - private void ComboBox_KeyEnterUpdate(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - ComboBox box = (ComboBox)sender; - DependencyProperty prop = ComboBox.TextProperty; - - BindingExpression binding = BindingOperations.GetBindingExpression(box, prop); - - if (binding is not null) - binding.UpdateSource(); - } - } } } From 1002199db467186ef4667706624f254c6eb759cf Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 20:49:58 +0100 Subject: [PATCH 098/111] Make uninstall process clearer --- .../Elements/Menu/Pages/InstallationPage.xaml | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml index 2790bc2..2499773 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/InstallationPage.xaml @@ -12,7 +12,7 @@ - + @@ -31,22 +31,36 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + From 9f9156b4cf30bd040879500ab5e0729fba2cdb4a Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 22:01:57 +0100 Subject: [PATCH 099/111] Make installation information clearer --- Bloxstrap/Bloxstrap.csproj | 1 + .../Resources/Menu/StartMenuLocation.png | Bin 0 -> 200155 bytes Bloxstrap/UI/Elements/Menu/MainWindow.xaml | 6 +- .../Elements/Menu/Pages/PreInstallPage.xaml | 29 +++++++++ .../Menu/Pages/PreInstallPage.xaml.cs | 13 ++++ .../UI/ViewModels/Menu/MainWindowViewModel.cs | 57 +++++++++++------- 6 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 Bloxstrap/Resources/Menu/StartMenuLocation.png create mode 100644 Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml create mode 100644 Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml.cs diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 5bd23b2..b5e501f 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -17,6 +17,7 @@ + diff --git a/Bloxstrap/Resources/Menu/StartMenuLocation.png b/Bloxstrap/Resources/Menu/StartMenuLocation.png new file mode 100644 index 0000000000000000000000000000000000000000..208e68d0a60a543556cf65ca645fa87bd3439f37 GIT binary patch literal 200155 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%0Dh|e>Ucg9 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

D%M#z}#$`yz|O~bwA{)ZnaPo4Wt`Np?j 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 - + @@ -42,6 +42,8 @@ + + @@ -68,7 +70,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml new file mode 100644 index 0000000..6b174da --- /dev/null +++ b/Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml.cs new file mode 100644 index 0000000..04793ef --- /dev/null +++ b/Bloxstrap/UI/Elements/Menu/Pages/PreInstallPage.xaml.cs @@ -0,0 +1,13 @@ +namespace Bloxstrap.UI.Menu.Pages +{ + ///

+ /// Interaction logic for PreInstallPage.xaml + /// + public partial class PreInstallPage + { + public PreInstallPage() + { + InitializeComponent(); + } + } +} diff --git a/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs index 2a239a4..91c88f5 100644 --- a/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs @@ -1,5 +1,8 @@ using System; +using System.ComponentModel; using System.IO; +using System.Linq; +using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -7,15 +10,17 @@ using Microsoft.Win32; using CommunityToolkit.Mvvm.Input; -using Wpf.Ui.Controls.Interfaces; using Wpf.Ui.Mvvm.Contracts; -using System.Linq; +using Bloxstrap.UI.Menu.Pages; namespace Bloxstrap.UI.ViewModels.Menu { - public class MainWindowViewModel + public class MainWindowViewModel : INotifyPropertyChanged { + public event PropertyChangedEventHandler? PropertyChanged; + public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + private readonly Window _window; private readonly IDialogService _dialogService; private readonly string _originalBaseDirectory = App.BaseDirectory; // we need this to check if the basedirectory changes @@ -23,7 +28,9 @@ namespace Bloxstrap.UI.ViewModels.Menu public ICommand CloseWindowCommand => new RelayCommand(CloseWindow); public ICommand ConfirmSettingsCommand => new RelayCommand(ConfirmSettings); + public Visibility NavigationVisibility { get; set; } = Visibility.Visible; public string ConfirmButtonText => App.IsFirstRun ? "Install" : "Save"; + public bool ConfirmButtonEnabled { get; set; } = true; public MainWindowViewModel(Window window, IDialogService dialogService) { @@ -90,7 +97,32 @@ namespace Bloxstrap.UI.ViewModels.Menu } } - if (!App.IsFirstRun) + if (App.IsFirstRun) + { + if (NavigationVisibility == Visibility.Visible) + { + ((INavigationWindow)_window).Navigate(typeof(PreInstallPage)); + + NavigationVisibility = Visibility.Collapsed; + ConfirmButtonEnabled = false; + + OnPropertyChanged(nameof(NavigationVisibility)); + OnPropertyChanged(nameof(ConfirmButtonEnabled)); + + Task.Run(async delegate + { + await Task.Delay(3000); + ConfirmButtonEnabled = true; + OnPropertyChanged(nameof(ConfirmButtonEnabled)); + }); + } + else + { + App.IsSetupComplete = true; + CloseWindow(); + } + } + else { App.ShouldSaveConfigs = true; App.FastFlags.Save(); @@ -112,23 +144,6 @@ namespace Bloxstrap.UI.ViewModels.Menu CloseWindow(); } - else - { - IDialogControl dialogControl = _dialogService.GetDialogControl(); - - dialogControl.ButtonRightClick += (_, _) => - { - dialogControl.Hide(); - App.IsSetupComplete = true; - CloseWindow(); - }; - - dialogControl.ShowAndWaitAsync( - "What to know before you install", - "After installation, you can open this menu again by searching for it in the Start menu.\n" + - "If you want to revert back to the original Roblox launcher, just uninstall Bloxstrap and it will automatically revert." - ); - } } } } From eeeb33fed2f99810bb987cbb9e3bb2cc3f8068c0 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 22:03:27 +0100 Subject: [PATCH 100/111] Update wpfui (ebe4974 -> 5f322e73b) --- wpfui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpfui b/wpfui index ebe4974..5f322e7 160000 --- a/wpfui +++ b/wpfui @@ -1 +1 @@ -Subproject commit ebe4974d29c5af8b9a236767054c8070b976a125 +Subproject commit 5f322e73b22d2bf3d0c052e845530b4e418d4be0 From 77725ea5d7d5d2bdcca8449cb2371a5b2e80ac72 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 22:30:52 +0100 Subject: [PATCH 101/111] Consolidate NotifyPropertyChanged ViewModels --- .../Bootstrapper/BootstrapperDialogViewModel.cs | 5 +---- .../Bootstrapper/ByfronDialogViewModel.cs | 2 +- .../UI/ViewModels/Menu/AppearanceViewModel.cs | 5 +---- .../UI/ViewModels/Menu/BehaviourViewModel.cs | 5 +---- .../UI/ViewModels/Menu/FastFlagsViewModel.cs | 5 +---- .../UI/ViewModels/Menu/InstallationViewModel.cs | 5 +---- .../UI/ViewModels/Menu/IntegrationsViewModel.cs | 5 +---- .../UI/ViewModels/Menu/MainWindowViewModel.cs | 5 +---- Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs | 5 +---- .../ViewModels/NotifyPropertyChangedViewModel.cs | 15 +++++++++++++++ 10 files changed, 24 insertions(+), 33 deletions(-) create mode 100644 Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs index 95866b6..5867d5c 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs @@ -9,11 +9,8 @@ using Bloxstrap.Extensions; namespace Bloxstrap.UI.ViewModels.Bootstrapper { - public class BootstrapperDialogViewModel : INotifyPropertyChanged + public class BootstrapperDialogViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private readonly IBootstrapperDialog _dialog; public ICommand CancelInstallCommand => new RelayCommand(CancelInstall); diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs index 4cc3d68..568fe34 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs @@ -8,7 +8,7 @@ using System.Windows.Media.Imaging; namespace Bloxstrap.UI.ViewModels.Bootstrapper { - public class ByfronDialogViewModel : BootstrapperDialogViewModel, INotifyPropertyChanged + public class ByfronDialogViewModel : BootstrapperDialogViewModel { // Using dark theme for default values. public ImageSource ByfronLogoLocation { get; set; } = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoDark.jpg")); diff --git a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs index c22ba1f..19a5224 100644 --- a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs @@ -15,11 +15,8 @@ using Bloxstrap.UI.Menu; namespace Bloxstrap.UI.ViewModels.Menu { - public class AppearanceViewModel : INotifyPropertyChanged + public class AppearanceViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private readonly Page _page; public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper); diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs index 331c997..0768819 100644 --- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs @@ -12,11 +12,8 @@ using Bloxstrap.Models; namespace Bloxstrap.UI.ViewModels.Menu { - public class BehaviourViewModel : INotifyPropertyChanged + public class BehaviourViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private bool _manualChannelEntry = !RobloxDeployment.SelectableChannels.Contains(App.Settings.Prop.Channel); public BehaviourViewModel() diff --git a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs index f1a1706..30ea370 100644 --- a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs @@ -10,11 +10,8 @@ using CommunityToolkit.Mvvm.Input; namespace Bloxstrap.UI.ViewModels.Menu { - public class FastFlagsViewModel : INotifyPropertyChanged + public class FastFlagsViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - public ICommand OpenClientSettingsCommand => new RelayCommand(OpenClientSettings); private void OpenClientSettings() => Utilities.ShellExecute(Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json")); diff --git a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs index 6ad864b..389abe8 100644 --- a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs @@ -6,11 +6,8 @@ using CommunityToolkit.Mvvm.Input; namespace Bloxstrap.UI.ViewModels.Menu { - public class InstallationViewModel : INotifyPropertyChanged + public class InstallationViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private string _originalInstallLocation = App.BaseDirectory; public ICommand BrowseInstallLocationCommand => new RelayCommand(BrowseInstallLocation); diff --git a/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs index 5847e6e..d0425ac 100644 --- a/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs @@ -8,11 +8,8 @@ using Bloxstrap.Models; namespace Bloxstrap.UI.ViewModels.Menu { - public class IntegrationsViewModel : INotifyPropertyChanged + public class IntegrationsViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - public ICommand AddIntegrationCommand => new RelayCommand(AddIntegration); public ICommand DeleteIntegrationCommand => new RelayCommand(DeleteIntegration); diff --git a/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs index 91c88f5..cad2e8d 100644 --- a/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs @@ -16,11 +16,8 @@ using Bloxstrap.UI.Menu.Pages; namespace Bloxstrap.UI.ViewModels.Menu { - public class MainWindowViewModel : INotifyPropertyChanged + public class MainWindowViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private readonly Window _window; private readonly IDialogService _dialogService; private readonly string _originalBaseDirectory = App.BaseDirectory; // we need this to check if the basedirectory changes diff --git a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs index 7859f4a..7616c58 100644 --- a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs @@ -15,11 +15,8 @@ using CommunityToolkit.Mvvm.Input; namespace Bloxstrap.UI.ViewModels.Menu { - public class ModsViewModel : INotifyPropertyChanged + public class ModsViewModel : NotifyPropertyChangedViewModel { - public event PropertyChangedEventHandler? PropertyChanged; - public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private void OpenModsFolder() => Process.Start("explorer.exe", Directories.Modifications); private string _customFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\CustomFont.ttf"); diff --git a/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs b/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs new file mode 100644 index 0000000..54acc1f --- /dev/null +++ b/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.UI.ViewModels +{ + public class NotifyPropertyChangedViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} From 282d4c665a2e8ddea521277d9377b128a37597f1 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 22:52:44 +0100 Subject: [PATCH 102/111] Consolidation - global usings --- Bloxstrap/App.xaml.cs | 20 +----- Bloxstrap/Bootstrapper.cs | 17 +---- Bloxstrap/Directories.cs | 5 +- Bloxstrap/Extensions/BootstrapperIconEx.cs | 5 +- Bloxstrap/Extensions/BootstrapperStyleEx.cs | 5 +- Bloxstrap/Extensions/CursorTypeEx.cs | 6 +- Bloxstrap/Extensions/DateTimeEx.cs | 6 +- Bloxstrap/Extensions/EmojiTypeEx.cs | 6 +- Bloxstrap/Extensions/IconEx.cs | 1 - Bloxstrap/Extensions/ThemeEx.cs | 1 - Bloxstrap/FastFlagManager.cs | 6 +- Bloxstrap/GlobalUsings.cs | 22 +++++++ Bloxstrap/HttpClientLoggingHandler.cs | 11 +--- Bloxstrap/Integrations/DiscordRichPresence.cs | 10 +-- Bloxstrap/Integrations/ServerNotifier.cs | 4 +- Bloxstrap/JsonManager.cs | 6 +- Bloxstrap/Logger.cs | 9 +-- .../Attributes/BuildMetadataAttribute.cs | 4 +- Bloxstrap/Models/ClientVersion.cs | 5 +- Bloxstrap/Models/DeployInfo.cs | 8 +-- Bloxstrap/Models/FontFace.cs | 5 +- Bloxstrap/Models/FontFamily.cs | 5 +- Bloxstrap/Models/GameMessage.cs | 4 +- Bloxstrap/Models/GithubRelease.cs | 5 +- Bloxstrap/Models/PackageManifest.cs | 5 -- .../Models/RobloxApi/ApiArrayResponse.cs | 5 +- Bloxstrap/Models/RobloxApi/GameCreator.cs | 4 +- .../Models/RobloxApi/GameDetailResponse.cs | 6 +- .../Models/RobloxApi/ThumbnailResponse.cs | 4 +- .../Models/RobloxApi/UniverseIdResponse.cs | 4 +- Bloxstrap/Models/Settings.cs | 2 - Bloxstrap/Models/State.cs | 6 +- Bloxstrap/ProtocolHandler.cs | 8 +-- Bloxstrap/Resource.cs | 5 +- Bloxstrap/RobloxActivity.cs | 12 +--- Bloxstrap/RobloxDeployment.cs | 11 +--- Bloxstrap/UI/Controls.cs | 5 +- .../Bootstrapper/Base/BaseFunctions.cs | 3 +- .../Bootstrapper/Base/WinFormsDialogBase.cs | 4 +- .../Bootstrapper/ByfronDialog.xaml.cs | 5 +- .../Bootstrapper/FluentDialog.xaml.cs | 5 +- .../Elements/Bootstrapper/LegacyDialog2008.cs | 1 - .../Elements/Bootstrapper/LegacyDialog2011.cs | 2 - .../Elements/Bootstrapper/ProgressDialog.cs | 5 +- .../UI/Elements/Bootstrapper/VistaDialog.cs | 4 +- Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs | 6 +- .../UI/Elements/FluentMessageBox.xaml.cs | 6 +- Bloxstrap/UI/Elements/Menu/MainWindow.xaml.cs | 4 +- .../Elements/Menu/Pages/BehaviourPage.xaml.cs | 7 +-- .../Elements/Menu/Pages/FastFlagsPage.xaml.cs | 3 +- .../Menu/Pages/IntegrationsPage.xaml.cs | 1 - .../UI/Elements/Menu/Pages/ModsPage.xaml.cs | 3 +- Bloxstrap/UI/IBootstrapperDialog.cs | 3 +- Bloxstrap/UI/Utility/WindowScaling.cs | 3 +- .../BootstrapperDialogViewModel.cs | 5 +- .../Bootstrapper/ByfronDialogViewModel.cs | 6 +- .../UI/ViewModels/Menu/AboutViewModel.cs | 6 +- .../UI/ViewModels/Menu/AppearanceViewModel.cs | 10 +-- .../UI/ViewModels/Menu/BehaviourViewModel.cs | 12 +--- .../UI/ViewModels/Menu/FastFlagsViewModel.cs | 7 +-- .../ViewModels/Menu/InstallationViewModel.cs | 4 +- .../ViewModels/Menu/IntegrationsViewModel.cs | 3 - .../UI/ViewModels/Menu/MainWindowViewModel.cs | 7 +-- Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs | 10 +-- .../NotifyPropertyChangedViewModel.cs | 7 +-- Bloxstrap/Updater.cs | 9 +-- Bloxstrap/Utilities.cs | 6 +- Bloxstrap/Utility/AsyncMutex.cs | 9 +-- Bloxstrap/Utility/Http.cs | 6 +- Bloxstrap/Utility/MD5Hash.cs | 62 +++++++++---------- Bloxstrap/Utility/NativeMethods.cs | 7 +-- Bloxstrap/Utility/SystemEvent.cs | 6 +- 72 files changed, 116 insertions(+), 384 deletions(-) create mode 100644 Bloxstrap/GlobalUsings.cs diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 4e7fb93..907dd8e 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -1,25 +1,9 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; +using System.Reflection; using System.Windows; using System.Windows.Threading; using Microsoft.Win32; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; -using Bloxstrap.Models; -using Bloxstrap.Models.Attributes; -using Bloxstrap.UI; -using Bloxstrap.Utility; - namespace Bloxstrap { /// @@ -27,8 +11,6 @@ namespace Bloxstrap /// public partial class App : Application { - public static readonly CultureInfo CultureFormat = CultureInfo.InvariantCulture; - public const string ProjectName = "Bloxstrap"; public const string ProjectRepository = "pizzaboxer/bloxstrap"; public const string RobloxAppName = "RobloxPlayerBeta"; diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index f9f263f..b3b359e 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1,24 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; +using System.Windows; using System.Windows.Forms; -using System.Windows; using Microsoft.Win32; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; using Bloxstrap.Integrations; -using Bloxstrap.Models; -using Bloxstrap.Tools; -using Bloxstrap.UI; namespace Bloxstrap { diff --git a/Bloxstrap/Directories.cs b/Bloxstrap/Directories.cs index faddb26..21f9d13 100644 --- a/Bloxstrap/Directories.cs +++ b/Bloxstrap/Directories.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; - -namespace Bloxstrap +namespace Bloxstrap { static class Directories { diff --git a/Bloxstrap/Extensions/BootstrapperIconEx.cs b/Bloxstrap/Extensions/BootstrapperIconEx.cs index dad9cef..90656a7 100644 --- a/Bloxstrap/Extensions/BootstrapperIconEx.cs +++ b/Bloxstrap/Extensions/BootstrapperIconEx.cs @@ -1,7 +1,4 @@ -using System; -using System.Drawing; - -using Bloxstrap.Enums; +using System.Drawing; namespace Bloxstrap.Extensions { diff --git a/Bloxstrap/Extensions/BootstrapperStyleEx.cs b/Bloxstrap/Extensions/BootstrapperStyleEx.cs index bb837d8..607c6b2 100644 --- a/Bloxstrap/Extensions/BootstrapperStyleEx.cs +++ b/Bloxstrap/Extensions/BootstrapperStyleEx.cs @@ -1,7 +1,4 @@ -using Bloxstrap.Enums; -using Bloxstrap.UI; - -namespace Bloxstrap.Extensions +namespace Bloxstrap.Extensions { static class BootstrapperStyleEx { diff --git a/Bloxstrap/Extensions/CursorTypeEx.cs b/Bloxstrap/Extensions/CursorTypeEx.cs index a46dc7e..34b8de9 100644 --- a/Bloxstrap/Extensions/CursorTypeEx.cs +++ b/Bloxstrap/Extensions/CursorTypeEx.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; - -using Bloxstrap.Enums; - -namespace Bloxstrap.Extensions +namespace Bloxstrap.Extensions { static class CursorTypeEx { diff --git a/Bloxstrap/Extensions/DateTimeEx.cs b/Bloxstrap/Extensions/DateTimeEx.cs index f30d011..62b4744 100644 --- a/Bloxstrap/Extensions/DateTimeEx.cs +++ b/Bloxstrap/Extensions/DateTimeEx.cs @@ -1,12 +1,10 @@ -using System; - -namespace Bloxstrap.Extensions +namespace Bloxstrap.Extensions { static class DateTimeEx { public static string ToFriendlyString(this DateTime dateTime) { - return dateTime.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", App.CultureFormat); + return dateTime.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt"); } } } diff --git a/Bloxstrap/Extensions/EmojiTypeEx.cs b/Bloxstrap/Extensions/EmojiTypeEx.cs index 07fd220..0eb9d6e 100644 --- a/Bloxstrap/Extensions/EmojiTypeEx.cs +++ b/Bloxstrap/Extensions/EmojiTypeEx.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; - -using Bloxstrap.Enums; - -namespace Bloxstrap.Extensions +namespace Bloxstrap.Extensions { static class EmojiTypeEx { diff --git a/Bloxstrap/Extensions/IconEx.cs b/Bloxstrap/Extensions/IconEx.cs index bdb1ddd..2cd48af 100644 --- a/Bloxstrap/Extensions/IconEx.cs +++ b/Bloxstrap/Extensions/IconEx.cs @@ -1,5 +1,4 @@ using System.Drawing; -using System.IO; using System.Windows.Media.Imaging; using System.Windows.Media; diff --git a/Bloxstrap/Extensions/ThemeEx.cs b/Bloxstrap/Extensions/ThemeEx.cs index 57ab955..30da539 100644 --- a/Bloxstrap/Extensions/ThemeEx.cs +++ b/Bloxstrap/Extensions/ThemeEx.cs @@ -1,5 +1,4 @@ using Microsoft.Win32; -using Bloxstrap.Enums; namespace Bloxstrap.Extensions { diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 0685e86..0043f6e 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Json; - -namespace Bloxstrap +namespace Bloxstrap { public class FastFlagManager : JsonManager> { diff --git a/Bloxstrap/GlobalUsings.cs b/Bloxstrap/GlobalUsings.cs new file mode 100644 index 0000000..2bfd51a --- /dev/null +++ b/Bloxstrap/GlobalUsings.cs @@ -0,0 +1,22 @@ +global using System; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.IO; +global using System.IO.Compression; +global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; +global using System.Linq; +global using System.Net; +global using System.Net.Http; +global using System.Threading; +global using System.Threading.Tasks; + +global using Bloxstrap.Enums; +global using Bloxstrap.Extensions; +global using Bloxstrap.Models; +global using Bloxstrap.Models.Attributes; +global using Bloxstrap.Models.RobloxApi; +global using Bloxstrap.UI; +global using Bloxstrap.Utility; \ No newline at end of file diff --git a/Bloxstrap/HttpClientLoggingHandler.cs b/Bloxstrap/HttpClientLoggingHandler.cs index ccf7bd2..a39b94a 100644 --- a/Bloxstrap/HttpClientLoggingHandler.cs +++ b/Bloxstrap/HttpClientLoggingHandler.cs @@ -1,13 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Bloxstrap +namespace Bloxstrap { internal class HttpClientLoggingHandler : MessageProcessingHandler { diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs index f6d4028..35628d0 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using DiscordRPC; - -using Bloxstrap.Models.RobloxApi; -using Bloxstrap.Models; +using DiscordRPC; namespace Bloxstrap.Integrations { diff --git a/Bloxstrap/Integrations/ServerNotifier.cs b/Bloxstrap/Integrations/ServerNotifier.cs index bfd55be..02e9b6a 100644 --- a/Bloxstrap/Integrations/ServerNotifier.cs +++ b/Bloxstrap/Integrations/ServerNotifier.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using System.Windows; +using System.Windows; namespace Bloxstrap.Integrations { diff --git a/Bloxstrap/JsonManager.cs b/Bloxstrap/JsonManager.cs index 095819a..38baeee 100644 --- a/Bloxstrap/JsonManager.cs +++ b/Bloxstrap/JsonManager.cs @@ -1,8 +1,4 @@ -using System; -using System.IO; -using System.Text.Json; - -namespace Bloxstrap +namespace Bloxstrap { public class JsonManager where T : new() { diff --git a/Bloxstrap/Logger.cs b/Bloxstrap/Logger.cs index aabc96e..3ef73ee 100644 --- a/Bloxstrap/Logger.cs +++ b/Bloxstrap/Logger.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.Threading; - -namespace Bloxstrap +namespace Bloxstrap { // https://stackoverflow.com/a/53873141/11852173 // TODO - this kind of sucks diff --git a/Bloxstrap/Models/Attributes/BuildMetadataAttribute.cs b/Bloxstrap/Models/Attributes/BuildMetadataAttribute.cs index 32423f3..7c8fbe0 100644 --- a/Bloxstrap/Models/Attributes/BuildMetadataAttribute.cs +++ b/Bloxstrap/Models/Attributes/BuildMetadataAttribute.cs @@ -1,6 +1,4 @@ -using System; - -namespace Bloxstrap.Models.Attributes +namespace Bloxstrap.Models.Attributes { [AttributeUsage(AttributeTargets.Assembly)] public class BuildMetadataAttribute : Attribute diff --git a/Bloxstrap/Models/ClientVersion.cs b/Bloxstrap/Models/ClientVersion.cs index 4f18c47..e951e4e 100644 --- a/Bloxstrap/Models/ClientVersion.cs +++ b/Bloxstrap/Models/ClientVersion.cs @@ -1,7 +1,4 @@ -using System; -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class ClientVersion { diff --git a/Bloxstrap/Models/DeployInfo.cs b/Bloxstrap/Models/DeployInfo.cs index 2f8f265..91c4f94 100644 --- a/Bloxstrap/Models/DeployInfo.cs +++ b/Bloxstrap/Models/DeployInfo.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class DeployInfo { diff --git a/Bloxstrap/Models/FontFace.cs b/Bloxstrap/Models/FontFace.cs index 50bb179..b8da43c 100644 --- a/Bloxstrap/Models/FontFace.cs +++ b/Bloxstrap/Models/FontFace.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class FontFace { diff --git a/Bloxstrap/Models/FontFamily.cs b/Bloxstrap/Models/FontFamily.cs index 9a846e1..1416a8f 100644 --- a/Bloxstrap/Models/FontFamily.cs +++ b/Bloxstrap/Models/FontFamily.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class FontFamily { diff --git a/Bloxstrap/Models/GameMessage.cs b/Bloxstrap/Models/GameMessage.cs index d7b1b98..7639652 100644 --- a/Bloxstrap/Models/GameMessage.cs +++ b/Bloxstrap/Models/GameMessage.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class GameMessage { diff --git a/Bloxstrap/Models/GithubRelease.cs b/Bloxstrap/Models/GithubRelease.cs index a388586..12b8876 100644 --- a/Bloxstrap/Models/GithubRelease.cs +++ b/Bloxstrap/Models/GithubRelease.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class GithubRelease { diff --git a/Bloxstrap/Models/PackageManifest.cs b/Bloxstrap/Models/PackageManifest.cs index 72eb799..4d7c3a1 100644 --- a/Bloxstrap/Models/PackageManifest.cs +++ b/Bloxstrap/Models/PackageManifest.cs @@ -4,11 +4,6 @@ * Copyright (c) 2015-present MaximumADHD */ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - namespace Bloxstrap.Models { public class PackageManifest : List diff --git a/Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs b/Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs index 7f7e948..4e1fefd 100644 --- a/Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs +++ b/Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.RobloxApi { /// /// Roblox.Web.WebAPI.Models.ApiArrayResponse diff --git a/Bloxstrap/Models/RobloxApi/GameCreator.cs b/Bloxstrap/Models/RobloxApi/GameCreator.cs index 3f4ef22..c191884 100644 --- a/Bloxstrap/Models/RobloxApi/GameCreator.cs +++ b/Bloxstrap/Models/RobloxApi/GameCreator.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.RobloxApi { /// /// Roblox.Games.Api.Models.Response.GameCreator diff --git a/Bloxstrap/Models/RobloxApi/GameDetailResponse.cs b/Bloxstrap/Models/RobloxApi/GameDetailResponse.cs index 2a824ac..dca8c81 100644 --- a/Bloxstrap/Models/RobloxApi/GameDetailResponse.cs +++ b/Bloxstrap/Models/RobloxApi/GameDetailResponse.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.RobloxApi { /// diff --git a/Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs b/Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs index 2a4a227..c667813 100644 --- a/Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs +++ b/Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.RobloxApi { /// /// Roblox.Web.Responses.Thumbnails.ThumbnailResponse diff --git a/Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs b/Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs index 19bb84d..c42d10a 100644 --- a/Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs +++ b/Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.RobloxApi { // lmao its just one property public class UniverseIdResponse diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index 95fd18f..4ba664a 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -1,7 +1,5 @@ using System.Collections.ObjectModel; -using Bloxstrap.Enums; - namespace Bloxstrap.Models { public class Settings diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index b099001..0e0a2e8 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; - -using Bloxstrap.Enums; - -namespace Bloxstrap.Models +namespace Bloxstrap.Models { public class State { diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/ProtocolHandler.cs index 4d4c58c..5d12b12 100644 --- a/Bloxstrap/ProtocolHandler.cs +++ b/Bloxstrap/ProtocolHandler.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Web; +using System.Web; using System.Windows; using Microsoft.Win32; -using Bloxstrap.Enums; -using Bloxstrap.UI; - namespace Bloxstrap { static class ProtocolHandler diff --git a/Bloxstrap/Resource.cs b/Bloxstrap/Resource.cs index a5a89bb..98df7e6 100644 --- a/Bloxstrap/Resource.cs +++ b/Bloxstrap/Resource.cs @@ -1,7 +1,4 @@ -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; +using System.Reflection; namespace Bloxstrap { diff --git a/Bloxstrap/RobloxActivity.cs b/Bloxstrap/RobloxActivity.cs index ca87f3c..a0e89e2 100644 --- a/Bloxstrap/RobloxActivity.cs +++ b/Bloxstrap/RobloxActivity.cs @@ -1,14 +1,4 @@ -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; - -using Bloxstrap.Models; - -namespace Bloxstrap +namespace Bloxstrap { public class RobloxActivity : IDisposable { diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index dc597f1..6667ca8 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -1,13 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; - -using Bloxstrap.Models; - -namespace Bloxstrap +namespace Bloxstrap { public static class RobloxDeployment { diff --git a/Bloxstrap/UI/Controls.cs b/Bloxstrap/UI/Controls.cs index 34115be..8d7faf8 100644 --- a/Bloxstrap/UI/Controls.cs +++ b/Bloxstrap/UI/Controls.cs @@ -1,8 +1,5 @@ -using System; -using System.Windows; +using System.Windows; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper; using Bloxstrap.UI.Menu; using Bloxstrap.UI.MessageBox; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/Base/BaseFunctions.cs b/Bloxstrap/UI/Elements/Bootstrapper/Base/BaseFunctions.cs index c3e3c08..8e493a3 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/Base/BaseFunctions.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/Base/BaseFunctions.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows; +using System.Windows; namespace Bloxstrap.UI.Elements.Bootstrapper.Base { diff --git a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs index 4627a0b..ba14b07 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs @@ -1,7 +1,5 @@ -using System; -using System.Windows.Forms; +using System.Windows.Forms; -using Bloxstrap.Extensions; using Bloxstrap.UI.Utility; namespace Bloxstrap.UI.Elements.Bootstrapper.Base diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs index a1d7385..88020ad 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs @@ -1,11 +1,8 @@ -using System; -using System.Windows; +using System.Windows; using System.Windows.Forms; using System.Windows.Media; using System.Windows.Media.Imaging; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper.Base; using Bloxstrap.UI.ViewModels.Bootstrapper; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs index fd3ec5e..ac3311f 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs @@ -1,12 +1,9 @@ -using System; -using System.Windows; -using System.Windows.Forms; +using System.Windows.Forms; using Wpf.Ui.Appearance; using Wpf.Ui.Mvvm.Contracts; using Wpf.Ui.Mvvm.Services; -using Bloxstrap.Extensions; using Bloxstrap.UI.ViewModels.Bootstrapper; using Bloxstrap.UI.Elements.Bootstrapper.Base; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs index a84042c..65d5251 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs @@ -1,4 +1,3 @@ -using System; using System.Windows.Forms; using Bloxstrap.UI.Elements.Bootstrapper.Base; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs index ae7657b..0588675 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs @@ -1,7 +1,5 @@ -using System; using System.Windows.Forms; -using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper.Base; namespace Bloxstrap.UI.Elements.Bootstrapper diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs b/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs index 8773d76..4f9bb01 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs @@ -1,9 +1,6 @@ -using System; -using System.Drawing; +using System.Drawing; using System.Windows.Forms; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper.Base; namespace Bloxstrap.UI.Elements.Bootstrapper diff --git a/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs b/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs index f102ad0..8a47f9c 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs @@ -1,7 +1,5 @@ -using System; -using System.Windows.Forms; +using System.Windows.Forms; -using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper.Base; namespace Bloxstrap.UI.Elements.Bootstrapper diff --git a/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs b/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs index 2b71bcb..6c07194 100644 --- a/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/ExceptionDialog.xaml.cs @@ -1,11 +1,7 @@ -using System; -using System.Diagnostics; -using System.Media; +using System.Media; using System.Windows; using System.Windows.Interop; -using Bloxstrap.Utility; - namespace Bloxstrap.UI { // hmm... do i use MVVM for this? diff --git a/Bloxstrap/UI/Elements/FluentMessageBox.xaml.cs b/Bloxstrap/UI/Elements/FluentMessageBox.xaml.cs index c04e178..19157f6 100644 --- a/Bloxstrap/UI/Elements/FluentMessageBox.xaml.cs +++ b/Bloxstrap/UI/Elements/FluentMessageBox.xaml.cs @@ -1,12 +1,10 @@ -using System; -using System.Configuration; -using System.Media; +using System.Media; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media.Imaging; + using Bloxstrap.UI.Utility; -using Bloxstrap.Utility; namespace Bloxstrap.UI.MessageBox { diff --git a/Bloxstrap/UI/Elements/Menu/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Menu/MainWindow.xaml.cs index 65fed5e..d974e52 100644 --- a/Bloxstrap/UI/Elements/Menu/MainWindow.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/MainWindow.xaml.cs @@ -1,12 +1,10 @@ -using System; -using System.Windows.Controls; +using System.Windows.Controls; using Wpf.Ui.Appearance; using Wpf.Ui.Controls.Interfaces; using Wpf.Ui.Mvvm.Contracts; using Wpf.Ui.Mvvm.Services; -using Bloxstrap.Extensions; using Bloxstrap.UI.ViewModels.Menu; namespace Bloxstrap.UI.Menu diff --git a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs index 736197b..f423e1b 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/BehaviourPage.xaml.cs @@ -1,9 +1,4 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Input; - -using Bloxstrap.UI.ViewModels.Menu; +using Bloxstrap.UI.ViewModels.Menu; namespace Bloxstrap.UI.Menu.Pages { diff --git a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml.cs index 7770a97..ccf4cff 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows.Input; +using System.Windows.Input; using Bloxstrap.UI.ViewModels.Menu; diff --git a/Bloxstrap/UI/Elements/Menu/Pages/IntegrationsPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/IntegrationsPage.xaml.cs index 397aeca..9210778 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/IntegrationsPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/IntegrationsPage.xaml.cs @@ -1,6 +1,5 @@ using System.Windows.Controls; -using Bloxstrap.Models; using Bloxstrap.UI.ViewModels.Menu; namespace Bloxstrap.UI.Menu.Pages diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs index 50cece0..c3173d2 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows; +using System.Windows; using Bloxstrap.UI.ViewModels.Menu; diff --git a/Bloxstrap/UI/IBootstrapperDialog.cs b/Bloxstrap/UI/IBootstrapperDialog.cs index 26b5aee..4b44900 100644 --- a/Bloxstrap/UI/IBootstrapperDialog.cs +++ b/Bloxstrap/UI/IBootstrapperDialog.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows.Forms; +using System.Windows.Forms; namespace Bloxstrap.UI { diff --git a/Bloxstrap/UI/Utility/WindowScaling.cs b/Bloxstrap/UI/Utility/WindowScaling.cs index 8ede97a..91447ad 100644 --- a/Bloxstrap/UI/Utility/WindowScaling.cs +++ b/Bloxstrap/UI/Utility/WindowScaling.cs @@ -1,5 +1,4 @@ -using System; -using System.Windows; +using System.Windows; using System.Windows.Forms; namespace Bloxstrap.UI.Utility diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs index 5867d5c..593921d 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs @@ -1,12 +1,9 @@ -using System.ComponentModel; -using System.Windows; +using System.Windows; using System.Windows.Input; using System.Windows.Media; using CommunityToolkit.Mvvm.Input; -using Bloxstrap.Extensions; - namespace Bloxstrap.UI.ViewModels.Bootstrapper { public class BootstrapperDialogViewModel : NotifyPropertyChangedViewModel diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs index 568fe34..29eb85a 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs @@ -1,8 +1,4 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Windows; +using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; diff --git a/Bloxstrap/UI/ViewModels/Menu/AboutViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/AboutViewModel.cs index 8291e12..5222f27 100644 --- a/Bloxstrap/UI/ViewModels/Menu/AboutViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/AboutViewModel.cs @@ -1,8 +1,4 @@ -using System; -using System.Windows; - -using Bloxstrap.Extensions; -using Bloxstrap.Models.Attributes; +using System.Windows; namespace Bloxstrap.UI.ViewModels.Menu { diff --git a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs index 19a5224..d81edfc 100644 --- a/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/AppearanceViewModel.cs @@ -1,16 +1,12 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Windows; +using System.Windows; using System.Windows.Controls; -using Microsoft.Win32; using System.Windows.Input; using System.Windows.Media; using CommunityToolkit.Mvvm.Input; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; +using Microsoft.Win32; + using Bloxstrap.UI.Menu; namespace Bloxstrap.UI.ViewModels.Menu diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs index 0768819..38b1a19 100644 --- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs @@ -1,14 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; - - -using Bloxstrap.Enums; -using Bloxstrap.Extensions; -using Bloxstrap.Models; +using System.Windows; namespace Bloxstrap.UI.ViewModels.Menu { diff --git a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs index 30ea370..3ba02f6 100644 --- a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Windows; +using System.Windows; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; diff --git a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs index 389abe8..c00093c 100644 --- a/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/InstallationViewModel.cs @@ -1,6 +1,4 @@ -using System.ComponentModel; -using System.Diagnostics; -using System.Windows.Input; +using System.Windows.Input; using CommunityToolkit.Mvvm.Input; diff --git a/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs index d0425ac..640b8b8 100644 --- a/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/IntegrationsViewModel.cs @@ -1,11 +1,8 @@ using System.Collections.ObjectModel; -using System.ComponentModel; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; -using Bloxstrap.Models; - namespace Bloxstrap.UI.ViewModels.Menu { public class IntegrationsViewModel : NotifyPropertyChangedViewModel diff --git a/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs index cad2e8d..9e51a3a 100644 --- a/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/MainWindowViewModel.cs @@ -1,9 +1,4 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; +using System.Windows; using System.Windows.Input; using Microsoft.Win32; diff --git a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs index 7616c58..3502350 100644 --- a/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/ModsViewModel.cs @@ -1,14 +1,6 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Windows; +using System.Windows; using System.Windows.Input; -using Bloxstrap.Enums; -using Bloxstrap.Extensions; - using Microsoft.Win32; using CommunityToolkit.Mvvm.Input; diff --git a/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs b/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs index 54acc1f..6cf587b 100644 --- a/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs +++ b/Bloxstrap/UI/ViewModels/NotifyPropertyChangedViewModel.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel; namespace Bloxstrap.UI.ViewModels { diff --git a/Bloxstrap/Updater.cs b/Bloxstrap/Updater.cs index b438634..39434e1 100644 --- a/Bloxstrap/Updater.cs +++ b/Bloxstrap/Updater.cs @@ -1,11 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Windows; -using System.Threading; -using System.Threading.Tasks; - -using Bloxstrap.UI; +using System.Windows; namespace Bloxstrap { diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 32194fa..c362a71 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -1,8 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; - -namespace Bloxstrap +namespace Bloxstrap { static class Utilities { diff --git a/Bloxstrap/Utility/AsyncMutex.cs b/Bloxstrap/Utility/AsyncMutex.cs index e6e972f..400ccc8 100644 --- a/Bloxstrap/Utility/AsyncMutex.cs +++ b/Bloxstrap/Utility/AsyncMutex.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Bloxstrap.Tools +namespace Bloxstrap.Utility { // https://gist.github.com/dfederm/35c729f6218834b764fa04c219181e4e diff --git a/Bloxstrap/Utility/Http.cs b/Bloxstrap/Utility/Http.cs index dbdec6b..3d1f402 100644 --- a/Bloxstrap/Utility/Http.cs +++ b/Bloxstrap/Utility/Http.cs @@ -1,8 +1,4 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Bloxstrap.Utility +namespace Bloxstrap.Utility { internal static class Http { diff --git a/Bloxstrap/Utility/MD5Hash.cs b/Bloxstrap/Utility/MD5Hash.cs index 9e1abcb..caa7e48 100644 --- a/Bloxstrap/Utility/MD5Hash.cs +++ b/Bloxstrap/Utility/MD5Hash.cs @@ -1,34 +1,28 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Utility -{ - public static class MD5Hash - { - public static string FromFile(string filename) - { - using (MD5 md5 = MD5.Create()) - { - using (FileStream stream = File.OpenRead(filename)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - - public static string FromBytes(byte[] data) - { - using (MD5 md5 = MD5.Create()) - { - byte[] hash = md5.ComputeHash(data); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } -} +using System.Security.Cryptography; + +namespace Bloxstrap.Utility +{ + public static class MD5Hash + { + public static string FromFile(string filename) + { + using (MD5 md5 = MD5.Create()) + { + using (FileStream stream = File.OpenRead(filename)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + + public static string FromBytes(byte[] data) + { + using (MD5 md5 = MD5.Create()) + { + byte[] hash = md5.ComputeHash(data); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } +} diff --git a/Bloxstrap/Utility/NativeMethods.cs b/Bloxstrap/Utility/NativeMethods.cs index 76cbbb6..321fbcf 100644 --- a/Bloxstrap/Utility/NativeMethods.cs +++ b/Bloxstrap/Utility/NativeMethods.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.InteropServices; namespace Bloxstrap.Utility { diff --git a/Bloxstrap/Utility/SystemEvent.cs b/Bloxstrap/Utility/SystemEvent.cs index 05eb590..f4b4746 100644 --- a/Bloxstrap/Utility/SystemEvent.cs +++ b/Bloxstrap/Utility/SystemEvent.cs @@ -4,11 +4,7 @@ * Copyright (c) 2015-present MaximumADHD */ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Bloxstrap.Tools +namespace Bloxstrap.Utility { public class SystemEvent : EventWaitHandle { From 0f1dd35efa8b94a3534c45a8b195fc6577607319 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 15 Jul 2023 23:10:52 +0100 Subject: [PATCH 103/111] Enable DPI scaling fixes by default --- Bloxstrap/FastFlagManager.cs | 2 ++ Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 0043f6e..197fd75 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -131,6 +131,8 @@ // set to 9999 by default if it doesnt already exist SetValueOnce("DFIntTaskSchedulerTargetFps", 9999); SetValueOnce("FFlagHandleAltEnterFullscreenManually", "False"); + SetValueOnce("DFFlagDisableDPIScale", "True"); + SetValueOnce("DFFlagVariableDPIScale2", "False"); } public override void Save() diff --git a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml index 5a49eaf..54ac918 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml @@ -73,11 +73,12 @@ + - + @@ -95,7 +96,7 @@ - + From 1a084015d32432a78bfd8da072ab895b31cb83c8 Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sun, 16 Jul 2023 13:47:45 +0100 Subject: [PATCH 104/111] fix warning without disabling warning --- Bloxstrap/App.xaml.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 907dd8e..fd65e2e 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -71,16 +71,14 @@ namespace Bloxstrap void FinalizeExceptionHandling(Exception exception) { -#pragma warning disable 162 #if DEBUG throw exception; -#endif - +#else if (!IsQuiet) Controls.ShowExceptionDialog(exception); Terminate(ErrorCode.ERROR_INSTALL_FAILURE); -#pragma warning restore 162 +#endif } protected override void OnStartup(StartupEventArgs e) From 8831786d80be82667e6a5e8930aea14c9bda51f5 Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sun, 16 Jul 2023 14:13:51 +0100 Subject: [PATCH 105/111] fix fluent cancel button --- Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml index 5916c07..eb8b10d 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml +++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml @@ -38,7 +38,7 @@ -