From 3eeebc7a8b63673017ae290f41e18ad91a5a5dea Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 5 Sep 2024 22:00:07 +0100 Subject: [PATCH 01/30] Draft: bootstrapper refactoring Roblox now installs to /Roblox/Player/ instead of /Versions/ This is a checkpoint commit. No mod manager, no error checking, no fullscreen optimizations configuration. Only installing and launching Roblox. THIS WORKED FIRST TRY BY THE WAY --- Bloxstrap/AppData/CommonAppData.cs | 9 + Bloxstrap/AppData/IAppData.cs | 16 +- Bloxstrap/AppData/RobloxPlayerData.cs | 14 +- Bloxstrap/AppData/RobloxStudioData.cs | 22 +- Bloxstrap/Bootstrapper.cs | 679 ++++++++---------- Bloxstrap/Integrations/ActivityWatcher.cs | 4 +- Bloxstrap/LaunchHandler.cs | 11 +- Bloxstrap/Models/ActivityData.cs | 2 +- Bloxstrap/Models/AppState.cs | 11 + Bloxstrap/Models/Manifest/PackageManifest.cs | 12 +- Bloxstrap/Models/State.cs | 10 +- Bloxstrap/Paths.cs | 2 + Bloxstrap/RobloxDeployment.cs | 57 +- .../ContextMenu/ServerInformationViewModel.cs | 2 - .../ViewModels/Settings/BehaviourViewModel.cs | 14 +- Bloxstrap/Utilities.cs | 2 +- Bloxstrap/Utility/AsyncHelpers.cs | 20 - Bloxstrap/Watcher.cs | 2 +- 18 files changed, 376 insertions(+), 513 deletions(-) create mode 100644 Bloxstrap/Models/AppState.cs delete mode 100644 Bloxstrap/Utility/AsyncHelpers.cs diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs index 5b202ab..16a1798 100644 --- a/Bloxstrap/AppData/CommonAppData.cs +++ b/Bloxstrap/AppData/CommonAppData.cs @@ -39,8 +39,17 @@ namespace Bloxstrap.AppData { "extracontent-places.zip", @"ExtraContent\places\" }, }; + public virtual string FinalDirectory { get; } = null!; + + public string StagingDirectory => $"{FinalDirectory}.staging"; + + public virtual string ExecutableName { get; } = null!; + + public string ExecutablePath => Path.Combine(FinalDirectory, ExecutableName); + public virtual IReadOnlyDictionary PackageDirectoryMap { get; set; } + public CommonAppData() { if (PackageDirectoryMap is null) diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs index c637027..719dd9b 100644 --- a/Bloxstrap/AppData/IAppData.cs +++ b/Bloxstrap/AppData/IAppData.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.AppData +namespace Bloxstrap.AppData { internal interface IAppData { @@ -18,6 +12,14 @@ namespace Bloxstrap.AppData string StartEvent { get; } + string FinalDirectory { get; } + + string StagingDirectory { get; } + + string ExecutablePath { get; } + + AppState State { get; } + IReadOnlyDictionary PackageDirectoryMap { get; set; } } } diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index 3bc8785..156d714 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -8,15 +8,19 @@ namespace Bloxstrap.AppData { public class RobloxPlayerData : CommonAppData, IAppData { - public string ProductName { get; } = "Roblox"; + public string ProductName => "Roblox"; - public string BinaryType { get; } = "WindowsPlayer"; + public string BinaryType => "WindowsPlayer"; - public string RegistryName { get; } = "RobloxPlayer"; + public string RegistryName => "RobloxPlayer"; - public string ExecutableName { get; } = "RobloxPlayerBeta.exe"; + public override string ExecutableName => "RobloxPlayerBeta.exe"; - public string StartEvent { get; } = "www.roblox.com/robloxStartedEvent"; + public string StartEvent => "www.roblox.com/robloxStartedEvent"; + + public override string FinalDirectory => Path.Combine(Paths.Roblox, "Player"); + + public AppState State => App.State.Prop.Player; public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 2c40ef8..18b1cca 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -1,22 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.AppData +namespace Bloxstrap.AppData { public class RobloxStudioData : CommonAppData, IAppData { - public string ProductName { get; } = "Roblox Studio"; + public string ProductName => "Roblox Studio"; - public string BinaryType { get; } = "WindowsStudio64"; + public string BinaryType => "WindowsStudio64"; - public string RegistryName { get; } = "RobloxStudio"; + public string RegistryName => "RobloxStudio"; - public string ExecutableName { get; } = "RobloxStudioBeta.exe"; + public override string ExecutableName => "RobloxStudioBeta.exe"; - public string StartEvent { get; } = "www.roblox.com/robloxStudioStartedEvent"; + public string StartEvent => "www.roblox.com/robloxStudioStartedEvent"; + + public override string FinalDirectory => Path.Combine(Paths.Roblox, "Studio"); + + public AppState State => App.State.Prop.Studio; public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 4ac7217..207fa9a 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -34,57 +34,18 @@ namespace Bloxstrap private readonly CancellationTokenSource _cancelTokenSource = new(); - private bool FreshInstall => String.IsNullOrEmpty(_versionGuid); - private IAppData AppData; - private string _playerLocation => Path.Combine(_versionFolder, AppData.ExecutableName); + private bool FreshInstall => String.IsNullOrEmpty(AppData.State.VersionGuid); private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; private LaunchMode _launchMode = App.LaunchSettings.RobloxLaunchMode; - private bool _installWebView2; - - private string _versionGuid - { - get - { - return _launchMode == LaunchMode.Player ? App.State.Prop.PlayerVersionGuid : App.State.Prop.StudioVersionGuid; - } - - set - { - if (_launchMode == LaunchMode.Player) - App.State.Prop.PlayerVersionGuid = value; - else - App.State.Prop.StudioVersionGuid = value; - } - } - - private int _distributionSize - { - get - { - return _launchMode == LaunchMode.Player ? App.State.Prop.PlayerSize : App.State.Prop.StudioSize; - } - - set - { - if (_launchMode == LaunchMode.Player) - App.State.Prop.PlayerSize = value; - else - App.State.Prop.StudioSize = value; - } - } - 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; @@ -92,14 +53,9 @@ namespace Bloxstrap #endregion #region Core - public Bootstrapper(bool installWebView2) + public Bootstrapper() { - _installWebView2 = installWebView2; - - if (_launchMode == LaunchMode.Player) - AppData = new RobloxPlayerData(); - else - AppData = new RobloxStudioData(); + AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData(); } private void SetStatus(string message) @@ -140,6 +96,8 @@ namespace Bloxstrap var connectionResult = await RobloxDeployment.InitializeConnectivity(); + App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); + if (connectionResult is not null) { App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed!"); @@ -154,17 +112,14 @@ namespace Bloxstrap else if (connectionResult.GetType() == typeof(AggregateException)) connectionResult = connectionResult.InnerException!; + // TODO: handle update skip Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, connectionResult); App.Terminate(ErrorCode.ERROR_CANCELLED); return; } - - App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); - - await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - + #if !DEBUG || DEBUG_UPDATER if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) { @@ -182,8 +137,8 @@ namespace Bloxstrap try { - Mutex.OpenExisting("Bloxstrap_SingletonMutex").Close(); - App.Logger.WriteLine(LOG_IDENT, "Bloxstrap_SingletonMutex mutex exists, waiting..."); + Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close(); + App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting..."); SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); mutexExists = true; } @@ -193,7 +148,7 @@ namespace Bloxstrap } // wait for mutex to be released if it's not yet - await using var mutex = new AsyncMutex(true, "Bloxstrap_SingletonMutex"); + await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper"); await mutex.AcquireAsync(_cancelTokenSource.Token); // reload our configs since they've likely changed by now @@ -203,37 +158,48 @@ namespace Bloxstrap App.State.Load(); } - await CheckLatestVersion(); + // TODO: handle exception and update skip + await GetLatestVersionInfo(); // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (_latestVersionGuid != _versionGuid || !File.Exists(_playerLocation)) + if (!File.Exists(AppData.ExecutablePath) || AppData.State.VersionGuid != _latestVersionGuid) await InstallLatestVersion(); - if (_installWebView2) - await InstallWebView2(); + //await ApplyModifications(); - await ApplyModifications(); + // 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 - // TODO: move this to install/upgrade flow - if (FreshInstall) - RegisterProgramSize(); + if (IsStudioLaunch) + { +#if STUDIO_FEATURES + ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application); + ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application); - CheckInstall(); - - // at this point we've finished updating our configs - App.State.Save(); + ProtocolHandler.RegisterRobloxPlace(Paths.Application); + ProtocolHandler.RegisterExtension(".rbxl"); + ProtocolHandler.RegisterExtension(".rbxlx"); +#endif + } + else + { + // TODO: there needs to be better helper functions for these + ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); + ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\""); + } await mutex.ReleaseAsync(); - if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelFired) + if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested) StartRoblox(); Dialog?.CloseBootstrapper(); } - private async Task CheckLatestVersion() + private async Task GetLatestVersionInfo() { - const string LOG_IDENT = "Bootstrapper::CheckLatestVersion"; + const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo"; // before we do anything, we need to query our channel // if it's set in the launch uri, we need to use it and set the registry key for it @@ -249,7 +215,7 @@ namespace Bloxstrap { channel = match.Groups[1].Value.ToLowerInvariant(); } - else if (key.GetValue("www.roblox.com") is string value) + else if (key.GetValue("www.roblox.com") is string value && !String.IsNullOrEmpty(value)) { channel = value; } @@ -285,8 +251,11 @@ namespace Bloxstrap key.SetValue("www.roblox.com", channel); _latestVersionGuid = clientVersion.VersionGuid; - _versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid); - _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); + + string pkgManifestUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt"); + var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl); + + _versionPackageManifest = new(pkgManifestData); } private void StartRoblox() @@ -313,9 +282,9 @@ namespace Bloxstrap var startInfo = new ProcessStartInfo() { - FileName = _playerLocation, + FileName = AppData.ExecutablePath, Arguments = _launchCommandLine, - WorkingDirectory = _versionFolder + WorkingDirectory = AppData.FinalDirectory }; if (_launchMode == LaunchMode.StudioAuth) @@ -340,7 +309,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid}), waiting for start event"); - startEventSignalled = startEvent.WaitOne(TimeSpan.FromSeconds(10)); + startEventSignalled = startEvent.WaitOne(TimeSpan.FromSeconds(30)); } if (!startEventSignalled) @@ -351,6 +320,9 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Start event signalled"); + if (IsStudioLaunch) + return; + var autoclosePids = new List(); // launch custom integrations now @@ -401,23 +373,23 @@ namespace Bloxstrap if (!_isInstalling) { + // TODO: this sucks and needs to be done better App.Terminate(ErrorCode.ERROR_CANCELLED); return; } - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; App.Logger.WriteLine(LOG_IDENT, "Cancelling install..."); _cancelTokenSource.Cancel(); - _cancelFired = true; try { // clean up install - if (Directory.Exists(_versionFolder)) - Directory.Delete(_versionFolder, true); + if (Directory.Exists(AppData.StagingDirectory)) + Directory.Delete(AppData.StagingDirectory, true); } catch (Exception ex) { @@ -432,47 +404,6 @@ namespace Bloxstrap #endregion #region App Install - public void RegisterProgramSize() - { - const string LOG_IDENT = "Bootstrapper::RegisterProgramSize"; - - App.Logger.WriteLine(LOG_IDENT, "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 distributionSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; - _distributionSize = distributionSize; - - int totalSize = App.State.Prop.PlayerSize + App.State.Prop.StudioSize; - - uninstallKey.SetValue("EstimatedSize", totalSize); - - App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); - } - - public static void CheckInstall() - { - const string LOG_IDENT = "Bootstrapper::CheckInstall"; - - App.Logger.WriteLine(LOG_IDENT, "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", Paths.Application, "-player \"%1\""); - ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\""); -#if STUDIO_FEATURES - ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application); - ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application); - - ProtocolHandler.RegisterRobloxPlace(Paths.Application); - ProtocolHandler.RegisterExtension(".rbxl"); - ProtocolHandler.RegisterExtension(".rbxlx"); -#endif - } - private async Task CheckForUpdates() { const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; @@ -588,26 +519,28 @@ namespace Bloxstrap Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Downloads); - Directory.CreateDirectory(Paths.Versions); + Directory.CreateDirectory(Paths.Roblox); + + if (Directory.Exists(AppData.StagingDirectory)) + Directory.Delete(AppData.StagingDirectory, true); + + Directory.CreateDirectory(AppData.StagingDirectory); // 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(Paths.Downloads); + var cachedPackages = Directory.GetFiles(Paths.Downloads); int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) { - Frontend.ShowMessageBox( - Strings.Bootstrapper_NotEnoughSpace, - MessageBoxImage.Error - ); - + Frontend.ShowMessageBox(Strings.Bootstrapper_NotEnoughSpace, MessageBoxImage.Error); App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); return; } if (Dialog is not null) { + // TODO: cancelling needs to always be enabled Dialog.CancelEnabled = true; Dialog.ProgressStyle = ProgressBarStyle.Continuous; @@ -617,9 +550,11 @@ namespace Bloxstrap _progressIncrement = (double)ProgressBarMaximum / _versionPackageManifest.Sum(package => package.PackedSize); } - foreach (Package package in _versionPackageManifest) + var extractionTasks = new List(); + + foreach (var package in _versionPackageManifest) { - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; // download all the packages synchronously @@ -631,333 +566,299 @@ namespace Bloxstrap // extract the package immediately after download asynchronously // discard is just used to suppress the warning - _ = Task.Run(() => ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}")); + extractionTasks.Add(Task.Run(() => ExtractPackage(package), _cancelTokenSource.Token)); } - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; - // allow progress bar to 100% before continuing (purely ux reasons lol) - await Task.Delay(1000); - if (Dialog is not null) { + // allow progress bar to 100% before continuing (purely ux reasons lol) + // TODO: come up with a better way of handling this that is non-blocking + await Task.Delay(1000); + Dialog.ProgressStyle = ProgressBarStyle.Marquee; SetStatus(Strings.Bootstrapper_Status_Configuring); } - // 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); - } - + // TODO: handle faulted tasks + await Task.WhenAll(extractionTasks); + App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); - string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); - await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + await File.WriteAllTextAsync(Path.Combine(AppData.StagingDirectory, "AppSettings.xml"), AppSettings); - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; - if (!FreshInstall) + if (FreshInstall) { - // let's take this opportunity to delete any packages we don't need anymore - foreach (string filename in cachedPackages) + using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); + using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); + + if (hklmKey is null && hkcuKey is null) { - if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) + var result = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes); + + if (result == MessageBoxResult.Yes) { - App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); - - try + App.Logger.WriteLine(LOG_IDENT, "Installing WebView2 runtime..."); + + var package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); + + if (package is null) { - File.Delete(filename); + App.Logger.WriteLine(LOG_IDENT, "Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); + return; } - catch (Exception ex) + + string baseDirectory = Path.Combine(AppData.StagingDirectory, AppData.PackageDirectoryMap[package.Name]); + + ExtractPackage(package); + + SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); + + var startInfo = new ProcessStartInfo() { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {filename}!"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } + WorkingDirectory = baseDirectory, + FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), + Arguments = "/silent /install" + }; - string oldVersionFolder = Path.Combine(Paths.Versions, _versionGuid); + await Process.Start(startInfo)!.WaitForExitAsync(); - // 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, AppData.ExecutableName); - string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); + App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); - if (appFlags is not null) - { - App.Logger.WriteLine(LOG_IDENT, $"Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); - appFlagsKey.SetValue(_playerLocation, appFlags); - appFlagsKey.DeleteValue(oldGameClientLocation); + Directory.Delete(baseDirectory, true); } } } - _versionGuid = _latestVersionGuid; + // finishing and cleanup - // 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 STUDIO_FEATURES - if (!Process.GetProcessesByName(App.RobloxPlayerAppName).Any() && !Process.GetProcessesByName(App.RobloxStudioAppName).Any()) -#else - if (!Process.GetProcessesByName(App.RobloxPlayerAppName).Any()) -#endif + AppData.State.VersionGuid = _latestVersionGuid; + + AppData.State.PackageHashes.Clear(); + + foreach (var package in _versionPackageManifest) + AppData.State.PackageHashes.Add(package.Name, package.Signature); + + var allPackageHashes = new List(); + + allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values); + allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values); + + foreach (string filename in cachedPackages) { - foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories()) + if (!allPackageHashes.Contains(filename)) { - if (dir.Name == App.State.Prop.PlayerVersionGuid || dir.Name == App.State.Prop.StudioVersionGuid || !dir.Name.StartsWith("version-")) - continue; - - App.Logger.WriteLine(LOG_IDENT, $"Removing old version folder for {dir.Name}"); - + App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); + try { - dir.Delete(true); + File.Delete(filename); } catch (Exception ex) { - App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {filename}!"); App.Logger.WriteException(LOG_IDENT, ex); } } } - // don't register program size until the program is registered, which will be done after this - if (!FreshInstall) - RegisterProgramSize(); + App.Logger.WriteLine(LOG_IDENT, "Registering approximate program size..."); + + int distributionSize = _versionPackageManifest.Sum(x => x.Size + x.PackedSize) / 1000; + + AppData.State.Size = distributionSize; + + int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size; + + using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) + { + uninstallKey.SetValue("EstimatedSize", totalSize); + } + + App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); if (Dialog is not null) Dialog.CancelEnabled = false; + if (Directory.Exists(AppData.FinalDirectory)) + { + try + { + // gross hack to see if roblox is still running + // i don't want to rely on mutexes because they can change, and will false flag for + // running installations that are not by bloxstrap + File.Delete(AppData.ExecutablePath); + + Directory.Delete(AppData.FinalDirectory, true); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, "Could not delete executable/folder, Roblox may still be running. Aborting update."); + App.Logger.WriteException(LOG_IDENT, ex); + + Directory.Delete(AppData.StagingDirectory); + + _isInstalling = false; + + return; + } + } + + Directory.Move(AppData.StagingDirectory, AppData.FinalDirectory); + + App.State.Save(); + _isInstalling = false; } - private async Task InstallWebView2() - { - const string LOG_IDENT = "Bootstrapper::InstallWebView2"; - - App.Logger.WriteLine(LOG_IDENT, "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(LOG_IDENT, "Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); - return; - } - - await ExtractPackage(package); - } - - SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); - - ProcessStartInfo startInfo = new() - { - WorkingDirectory = baseDirectory, - FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), - Arguments = "/silent /install" - }; - - await Process.Start(startInfo)!.WaitForExitAsync(); - - App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); - } - - private async Task ApplyModifications() - { - const string LOG_IDENT = "Bootstrapper::ApplyModifications"; + //private async Task ApplyModifications() + //{ + // const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) - { - App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); - return; - } + // if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) + // { + // App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); + // return; + // } - SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); + // SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); - // set executable flags for fullscreen optimizations - App.Logger.WriteLine(LOG_IDENT, "Checking executable flags..."); - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; - string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation); + // // handle file mods + // App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); - 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)) - { - App.Logger.WriteLine(LOG_IDENT, $"Deleting flag '{flag.Trim()}'"); + // // manifest has been moved to State.json + // File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); - // 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); - } + // List modFolderFiles = new(); - // hmm, maybe make a unified handler for this? this is just lazily copy pasted from above + // if (!Directory.Exists(Paths.Modifications)) + // Directory.CreateDirectory(Paths.Modifications); - flag = " RUNASADMIN"; - appFlags = (string?)appFlagsKey.GetValue(_playerLocation); + // // check custom font mod + // // instead of replacing the fonts themselves, we'll just alter the font family manifests - if (appFlags is not null && appFlags.Contains(flag)) - { - App.Logger.WriteLine(LOG_IDENT, $"Deleting flag '{flag.Trim()}'"); - - // 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); - } - } + // string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - // handle file mods - App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); + // if (File.Exists(Paths.CustomFont)) + // { + // App.Logger.WriteLine(LOG_IDENT, "Begin font check"); - // manifest has been moved to State.json - File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); + // Directory.CreateDirectory(modFontFamiliesFolder); - List modFolderFiles = new(); + // foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families"))) + // { + // string jsonFilename = Path.GetFileName(jsonFilePath); + // string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); - if (!Directory.Exists(Paths.Modifications)) - Directory.CreateDirectory(Paths.Modifications); + // if (File.Exists(modFilepath)) + // continue; - // check custom font mod - // instead of replacing the fonts themselves, we'll just alter the font family manifests + // App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); - string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); + // FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); - if (File.Exists(Paths.CustomFont)) - { - App.Logger.WriteLine(LOG_IDENT, "Begin font check"); + // if (fontFamilyData is null) + // continue; - Directory.CreateDirectory(modFontFamiliesFolder); + // foreach (FontFace fontFace in fontFamilyData.Faces) + // fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; - foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families"))) - { - string jsonFilename = Path.GetFileName(jsonFilePath); - string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); + // // TODO: writing on every launch is not necessary + // File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); + // } - if (File.Exists(modFilepath)) - continue; + // App.Logger.WriteLine(LOG_IDENT, "End font check"); + // } + // else if (Directory.Exists(modFontFamiliesFolder)) + // { + // Directory.Delete(modFontFamiliesFolder, true); + // } - App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); + // foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) + // { + // // get relative directory path + // string relativeFile = file.Substring(Paths.Modifications.Length + 1); - FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); + // // v1.7.0 - README has been moved to the preferences menu now + // if (relativeFile == "README.txt") + // { + // File.Delete(file); + // continue; + // } - if (fontFamilyData is null) - continue; + // if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) + // continue; - foreach (FontFace fontFace in fontFamilyData.Faces) - fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; + // if (relativeFile.EndsWith(".lock")) + // continue; - // TODO: writing on every launch is not necessary - File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); - } + // modFolderFiles.Add(relativeFile); - App.Logger.WriteLine(LOG_IDENT, "End font check"); - } - else if (Directory.Exists(modFontFamiliesFolder)) - { - Directory.Delete(modFontFamiliesFolder, true); - } + // string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); + // string fileVersionFolder = Path.Combine(_versionFolder, relativeFile); - foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) - { - // get relative directory path - string relativeFile = file.Substring(Paths.Modifications.Length + 1); + // if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) + // { + // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); + // continue; + // } - // v1.7.0 - README has been moved to the preferences menu now - if (relativeFile == "README.txt") - { - File.Delete(file); - continue; - } + // Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); - if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) - continue; + // Filesystem.AssertReadOnly(fileVersionFolder); + // File.Copy(fileModFolder, fileVersionFolder, true); + // Filesystem.AssertReadOnly(fileVersionFolder); - if (relativeFile.EndsWith(".lock")) - continue; + // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); + // } - modFolderFiles.Add(relativeFile); + // // the manifest is primarily here to keep track of what files have been + // // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages + // // now check for files that have been deleted from the mod folder according to the manifest - string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); - string fileVersionFolder = Path.Combine(_versionFolder, relativeFile); + // // TODO: this needs to extract the files from packages in bulk, this is way too slow + // foreach (string fileLocation in App.State.Prop.ModManifest) + // { + // if (modFolderFiles.Contains(fileLocation)) + // continue; - if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) - { - App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); - continue; - } + // var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); - Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); + // // package doesn't exist, likely mistakenly placed file + // if (String.IsNullOrEmpty(package.Key)) + // { + // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); - Filesystem.AssertReadOnly(fileVersionFolder); - File.Copy(fileModFolder, fileVersionFolder, true); - Filesystem.AssertReadOnly(fileVersionFolder); + // string versionFileLocation = Path.Combine(_versionFolder, fileLocation); - App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); - } + // if (File.Exists(versionFileLocation)) + // File.Delete(versionFileLocation); - // 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 + // continue; + // } - // TODO: this needs to extract the files from packages in bulk, this is way too slow - foreach (string fileLocation in App.State.Prop.ModManifest) - { - if (modFolderFiles.Contains(fileLocation)) - continue; + // // restore original file + // string fileName = fileLocation.Substring(package.Value.Length); + // await ExtractFileFromPackage(package.Key, fileName); - var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}"); + // } - // package doesn't exist, likely mistakenly placed file - if (String.IsNullOrEmpty(package.Key)) - { - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); + // App.State.Prop.ModManifest = modFolderFiles; + // App.State.Save(); - string versionFileLocation = Path.Combine(_versionFolder, fileLocation); - - if (File.Exists(versionFileLocation)) - File.Delete(versionFileLocation); - - continue; - } - - // restore original file - string fileName = fileLocation.Substring(package.Value.Length); - await ExtractFileFromPackage(package.Key, fileName); - - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}"); - } - - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); - - App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); - } + // App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); + //} private async Task DownloadPackage(Package package) { string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}"; - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); @@ -966,7 +867,7 @@ namespace Bloxstrap if (File.Exists(packageLocation)) { - FileInfo file = new(packageLocation); + var file = new FileInfo(packageLocation); string calculatedMD5 = MD5Hash.FromFile(packageLocation); @@ -1002,6 +903,9 @@ namespace Bloxstrap if (File.Exists(packageLocation)) return; + // TODO: telemetry for this. chances are that this is completely unnecessary and that it can be removed. + // but, we need to ensure this doesn't work before we can do that. + const int maxTries = 5; App.Logger.WriteLine(LOG_IDENT, "Downloading..."); @@ -1010,7 +914,7 @@ namespace Bloxstrap for (int i = 1; i <= maxTries; i++) { - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; int totalBytesRead = 0; @@ -1023,7 +927,7 @@ namespace Bloxstrap while (true) { - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) { stream.Close(); fileStream.Close(); @@ -1087,15 +991,12 @@ namespace Bloxstrap } } - private Task ExtractPackage(Package package) + private void ExtractPackage(Package package) { const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - if (_cancelFired) - return Task.CompletedTask; - string packageLocation = Path.Combine(Paths.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name]); + string packageFolder = Path.Combine(AppData.StagingDirectory, AppData.PackageDirectoryMap[package.Name]); App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); @@ -1103,31 +1004,27 @@ namespace Bloxstrap fastZip.ExtractZip(packageLocation, packageFolder, null); App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); - - _packagesExtracted += 1; - - return Task.CompletedTask; } - private async Task ExtractFileFromPackage(string packageName, string fileName) - { - Package? package = _versionPackageManifest.Find(x => x.Name == packageName); + //private async Task ExtractFileFromPackage(string packageName, string fileName) + //{ + // Package? package = _versionPackageManifest.Find(x => x.Name == packageName); - if (package is null) - return; + // if (package is null) + // return; - await DownloadPackage(package); + // await DownloadPackage(package); - using ZipArchive archive = ZipFile.OpenRead(Path.Combine(Paths.Downloads, package.Signature)); + // using ZipArchive archive = ZipFile.OpenRead(Path.Combine(Paths.Downloads, package.Signature)); - ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); + // ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); - if (entry is null) - return; + // if (entry is null) + // return; - string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName); - entry.ExtractToFile(extractionPath, true); - } + // string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName); + // entry.ExtractToFile(extractionPath, true); + //} #endregion } } diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index 41acd93..673e195 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -1,6 +1,4 @@ -using System.Windows; - -namespace Bloxstrap.Integrations +namespace Bloxstrap.Integrations { public class ActivityWatcher : IDisposable { diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 0c00a4c..502936a 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -169,15 +169,6 @@ namespace Bloxstrap App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND); } - bool installWebView2 = false; - { - using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - - if (hklmKey is null && hkcuKey is null) - installWebView2 = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes) == MessageBoxResult.Yes; - } - if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _)) { // this currently doesn't work very well since it relies on checking the existence of the singleton mutex @@ -195,7 +186,7 @@ namespace Bloxstrap // start bootstrapper and show the bootstrapper modal if we're not running silently App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); - var bootstrapper = new Bootstrapper(installWebView2); + var bootstrapper = new Bootstrapper(); IBootstrapperDialog? dialog = null; if (!App.LaunchSettings.QuietFlag.Active) diff --git a/Bloxstrap/Models/ActivityData.cs b/Bloxstrap/Models/ActivityData.cs index 69c9499..3a940e6 100644 --- a/Bloxstrap/Models/ActivityData.cs +++ b/Bloxstrap/Models/ActivityData.cs @@ -137,7 +137,7 @@ namespace Bloxstrap.Models private void RejoinServer() { - string playerPath = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe"); + string playerPath = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe"); Process.Start(playerPath, GetInviteDeeplink(false)); } diff --git a/Bloxstrap/Models/AppState.cs b/Bloxstrap/Models/AppState.cs new file mode 100644 index 0000000..68c3c58 --- /dev/null +++ b/Bloxstrap/Models/AppState.cs @@ -0,0 +1,11 @@ +namespace Bloxstrap.Models +{ + public class AppState + { + public string VersionGuid { get; set; } = String.Empty; + + public Dictionary PackageHashes { get; set; } = new(); + + public int Size { get; set; } + } +} diff --git a/Bloxstrap/Models/Manifest/PackageManifest.cs b/Bloxstrap/Models/Manifest/PackageManifest.cs index 13c7172..d46e474 100644 --- a/Bloxstrap/Models/Manifest/PackageManifest.cs +++ b/Bloxstrap/Models/Manifest/PackageManifest.cs @@ -8,9 +8,9 @@ namespace Bloxstrap.Models.Manifest { public class PackageManifest : List { - private PackageManifest(string data) + public PackageManifest(string data) { - using StringReader reader = new StringReader(data); + using var reader = new StringReader(data); string? version = reader.ReadLine(); if (version != "v0") @@ -46,13 +46,5 @@ namespace Bloxstrap.Models.Manifest }); } } - - public static async Task Get(string versionGuid) - { - string pkgManifestUrl = RobloxDeployment.GetLocation($"/{versionGuid}-rbxPkgManifest.txt"); - var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl); - - return new PackageManifest(pkgManifestData); - } } } diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index 49ece20..9aeea4a 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -4,13 +4,9 @@ { public bool ShowFFlagEditorWarning { get; set; } = true; - [Obsolete("Use PlayerVersionGuid instead", true)] - public string VersionGuid { set { PlayerVersionGuid = value; } } - public string PlayerVersionGuid { get; set; } = ""; - public string StudioVersionGuid { get; set; } = ""; - - public int PlayerSize { get; set; } = 0; - public int StudioSize { get; set; } = 0; + public AppState Player { get; set; } = new(); + + public AppState Studio { get; set; } = new(); public List ModManifest { get; set; } = new(); } diff --git a/Bloxstrap/Paths.cs b/Bloxstrap/Paths.cs index 43d4a1c..7240c6f 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -22,6 +22,7 @@ public static string Integrations { get; private set; } = ""; public static string Versions { get; private set; } = ""; public static string Modifications { get; private set; } = ""; + public static string Roblox { get; private set; } = ""; public static string Application { get; private set; } = ""; @@ -37,6 +38,7 @@ Integrations = Path.Combine(Base, "Integrations"); Versions = Path.Combine(Base, "Versions"); Modifications = Path.Combine(Base, "Modifications"); + Roblox = Path.Combine(Base, "Roblox"); Application = Path.Combine(Base, $"{App.ProjectName}.exe"); } diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index 37784a5..d23a5aa 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -15,6 +15,7 @@ private static readonly Dictionary BaseUrls = new() { { "https://setup.rbxcdn.com", 0 }, + { "https://setup-aws.rbxcdn.com", 2 }, { "https://setup-ak.rbxcdn.com", 2 }, { "https://roblox-setup.cachefly.net", 2 }, { "https://s3.amazonaws.com/setup.roblox.com", 4 } @@ -22,7 +23,7 @@ private static async Task TestConnection(string url, int priority, CancellationToken token) { - string LOG_IDENT = $"RobloxDeployment::TestConnection.{url}"; + string LOG_IDENT = $"RobloxDeployment::TestConnection<{url}>"; await Task.Delay(priority * 1000, token); @@ -38,8 +39,9 @@ // versionStudio is the version hash for the last MFC studio to be deployed. // the response body should always be "version-012732894899482c". string content = await response.Content.ReadAsStringAsync(token); + if (content != VersionStudioHash) - throw new Exception($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")"); + throw new InvalidHTTPResponseException($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")"); } catch (TaskCanceledException) { @@ -66,11 +68,10 @@ // returns null for success - CancellationTokenSource tokenSource = new CancellationTokenSource(); - CancellationToken token = tokenSource.Token; + var tokenSource = new CancellationTokenSource(); var exceptions = new List(); - var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, token)).ToList(); + var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, tokenSource.Token)).ToList(); App.Logger.WriteLine(LOG_IDENT, "Testing connectivity..."); @@ -127,7 +128,11 @@ App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel}"); + if (String.IsNullOrEmpty(channel)) + channel = DefaultChannel; + string cacheKey = $"{channel}-{binaryType}"; + ClientVersion clientVersion; if (ClientVersionCache.ContainsKey(cacheKey)) @@ -137,57 +142,37 @@ } else { + bool isDefaultChannel = String.Compare(channel, DefaultChannel, StringComparison.OrdinalIgnoreCase) == 0; + string path = $"/v2/client-version/{binaryType}"; - if (String.Compare(channel, DefaultChannel, StringComparison.InvariantCultureIgnoreCase) != 0) + if (!isDefaultChannel) path = $"/v2/client-version/{binaryType}/channel/{channel}"; - HttpResponseMessage deployInfoResponse; - try { - deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path); + clientVersion = await Http.GetJson($"https://clientsettingscdn.roblox.com/{path}"); } catch (Exception ex) { App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings..."); App.Logger.WriteException(LOG_IDENT, ex); - deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettings.roblox.com" + path); + clientVersion = await Http.GetJson($"https://clientsettings.roblox.com/{path}"); } - string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync(); - - if (!deployInfoResponse.IsSuccessStatusCode) + // check if channel is behind LIVE + if (!isDefaultChannel) { - // 400 = Invalid binaryType. - // 404 = Could not find version details for binaryType. - // 500 = Error while fetching version information. - // either way, we throw + var defaultClientVersion = await GetInfo(DefaultChannel); - App.Logger.WriteLine(LOG_IDENT, - "Failed to fetch deploy info!\r\n" + - $"\tStatus code: {deployInfoResponse.StatusCode}\r\n" + - $"\tResponse: {rawResponse}" - ); - - throw new HttpResponseException(deployInfoResponse); + if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan) + clientVersion.IsBehindDefaultChannel = true; } - clientVersion = JsonSerializer.Deserialize(rawResponse)!; + ClientVersionCache[cacheKey] = clientVersion; } - // check if channel is behind LIVE - if (channel != DefaultChannel) - { - var defaultClientVersion = await GetInfo(DefaultChannel); - - if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan) - clientVersion.IsBehindDefaultChannel = true; - } - - ClientVersionCache[cacheKey] = clientVersion; - return clientVersion; } } diff --git a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs index ebf1384..420b916 100644 --- a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs @@ -19,8 +19,6 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu public ICommand CopyInstanceIdCommand => new RelayCommand(CopyInstanceId); - public EventHandler? RequestCloseEvent; - public ServerInformationViewModel(Watcher watcher) { _activityWatcher = watcher.ActivityWatcher!; diff --git a/Bloxstrap/UI/ViewModels/Settings/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/BehaviourViewModel.cs index bf205ff..475bfbd 100644 --- a/Bloxstrap/UI/ViewModels/Settings/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/BehaviourViewModel.cs @@ -27,20 +27,20 @@ { // wouldnt it be better to check old version guids? // what about fresh installs? - get => String.IsNullOrEmpty(App.State.Prop.PlayerVersionGuid) && String.IsNullOrEmpty(App.State.Prop.StudioVersionGuid); + get => String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid); set { if (value) { - _oldPlayerVersionGuid = App.State.Prop.PlayerVersionGuid; - _oldStudioVersionGuid = App.State.Prop.StudioVersionGuid; - App.State.Prop.PlayerVersionGuid = ""; - App.State.Prop.StudioVersionGuid = ""; + _oldPlayerVersionGuid = App.State.Prop.Player.VersionGuid; + _oldStudioVersionGuid = App.State.Prop.Studio.VersionGuid; + App.State.Prop.Player.VersionGuid = ""; + App.State.Prop.Studio.VersionGuid = ""; } else { - App.State.Prop.PlayerVersionGuid = _oldPlayerVersionGuid; - App.State.Prop.StudioVersionGuid = _oldStudioVersionGuid; + App.State.Prop.Player.VersionGuid = _oldPlayerVersionGuid; + App.State.Prop.Studio.VersionGuid = _oldStudioVersionGuid; } } } diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 4613d01..2b28ae7 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -51,7 +51,7 @@ namespace Bloxstrap public static string GetRobloxVersion(bool studio) { - string versionGuid = studio ? App.State.Prop.StudioVersionGuid : App.State.Prop.PlayerVersionGuid; + string versionGuid = studio ? App.State.Prop.Studio.VersionGuid : App.State.Prop.Player.VersionGuid; string fileName = studio ? "RobloxStudioBeta.exe" : "RobloxPlayerBeta.exe"; string playerLocation = Path.Combine(Paths.Versions, versionGuid, fileName); diff --git a/Bloxstrap/Utility/AsyncHelpers.cs b/Bloxstrap/Utility/AsyncHelpers.cs deleted file mode 100644 index d6a53f8..0000000 --- a/Bloxstrap/Utility/AsyncHelpers.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Bloxstrap.Utility -{ - public static class AsyncHelpers - { - public static void ExceptionHandler(Task task, object? state) - { - const string LOG_IDENT = "AsyncHelpers::ExceptionHandler"; - - if (task.Exception is null) - return; - - if (state is null) - App.Logger.WriteLine(LOG_IDENT, "An exception occurred while running the task"); - else - App.Logger.WriteLine(LOG_IDENT, $"An exception occurred while running the task '{state}'"); - - App.FinalizeExceptionHandling(task.Exception); - } - } -} diff --git a/Bloxstrap/Watcher.cs b/Bloxstrap/Watcher.cs index 9b14f88..86859a8 100644 --- a/Bloxstrap/Watcher.cs +++ b/Bloxstrap/Watcher.cs @@ -31,7 +31,7 @@ namespace Bloxstrap #if DEBUG if (String.IsNullOrEmpty(watcherData)) { - string path = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe"); + string path = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe"); using var gameClientProcess = Process.Start(path); _gameClientPid = gameClientProcess.Id; } From e6f70a0c82508057339cfed8742ec6e675ec57d5 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 6 Sep 2024 09:59:49 +0100 Subject: [PATCH 02/30] Remove option for disabling fullscreen optimizations The option was only introduced because Roblox's installation path wasn't static. Now that it is, this option doesn't need to exist anymore, and it isn't an issue for people to just set it manually now. --- Bloxstrap/Models/Settings.cs | 1 - Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml | 8 -------- Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml.cs | 8 +------- Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs | 6 ------ 4 files changed, 1 insertion(+), 22 deletions(-) diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index 11ce9c3..ebe594f 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -25,6 +25,5 @@ namespace Bloxstrap.Models // mod preset configuration public bool UseDisableAppPatch { get; set; } = false; - public bool DisableFullscreenOptimizations { get; set; } = false; } } diff --git a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml index bb0e06f..af34a12 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml @@ -93,13 +93,5 @@ - - - - diff --git a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml.cs b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml.cs index 4e7e204..dc82f08 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml.cs @@ -1,6 +1,4 @@ -using System.Windows; - -using Bloxstrap.UI.ViewModels.Settings; +using Bloxstrap.UI.ViewModels.Settings; namespace Bloxstrap.UI.Elements.Settings.Pages { @@ -13,10 +11,6 @@ namespace Bloxstrap.UI.Elements.Settings.Pages { DataContext = new ModsViewModel(); InitializeComponent(); - - // fullscreen optimizations were only added in windows 10 build 17093 - if (Environment.OSVersion.Version.Build < 17093) - this.FullscreenOptimizationsToggle.Visibility = Visibility.Collapsed; } } } diff --git a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs index f36b897..0e85f6c 100644 --- a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs @@ -95,11 +95,5 @@ namespace Bloxstrap.UI.ViewModels.Settings }); public FontModPresetTask TextFontTask { get; } = new(); - - public bool DisableFullscreenOptimizations - { - get => App.Settings.Prop.DisableFullscreenOptimizations; - set => App.Settings.Prop.DisableFullscreenOptimizations = value; - } } } From 0189431357154f762dd54ea51d91d679cf7ee4e5 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 7 Sep 2024 13:26:23 +0100 Subject: [PATCH 03/30] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 424babb..12e90d4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -8,6 +8,7 @@ body: ### **Preliminary instructions** - Before opening an issue, please [check the Wiki first](https://github.com/pizzaboxer/bloxstrap/wiki/) to see if your problem has been addressed there. - If it isn't, please confirm which pages that you read that were relevant to your issue. + - Your issue ***will*** be closed without warning if there's a Wiki page addressing your problem. - If your problem is with Roblox itself (i.e. it crashes or doesn't launch), [check to see if it happens without Bloxstrap](https://github.com/pizzaboxer/bloxstrap/wiki/Roblox-crashes-or-does-not-launch). - Please only open an issue if your problem happens only with Bloxstrap, and state clearly that this is the case, as anything else is out of my control. - If you are getting a Bloxstrap Exception error, please attach a copy of the provided log file. There is a button on the dialog that locates it for you. From 99ff002121c42ec93ddc50551d534c3d3c88232f Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 7 Sep 2024 22:50:45 +0100 Subject: [PATCH 04/30] Abandon staging folder idea, use lockfile instead thank you matt for helping me see sense through this one --- Bloxstrap/AppData/CommonAppData.cs | 10 +- Bloxstrap/AppData/IAppData.cs | 4 +- Bloxstrap/AppData/RobloxPlayerData.cs | 2 +- Bloxstrap/AppData/RobloxStudioData.cs | 2 +- Bloxstrap/Bootstrapper.cs | 120 ++++++++++-------- .../Bootstrapper/Base/WinFormsDialogBase.cs | 2 +- .../Bootstrapper/ByfronDialog.xaml.cs | 2 +- .../Bootstrapper/ClassicFluentDialog.xaml.cs | 2 +- .../Bootstrapper/FluentDialog.xaml.cs | 2 +- .../BootstrapperDialogViewModel.cs | 2 +- 10 files changed, 79 insertions(+), 69 deletions(-) diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs index 16a1798..c74003a 100644 --- a/Bloxstrap/AppData/CommonAppData.cs +++ b/Bloxstrap/AppData/CommonAppData.cs @@ -39,13 +39,13 @@ namespace Bloxstrap.AppData { "extracontent-places.zip", @"ExtraContent\places\" }, }; - public virtual string FinalDirectory { get; } = null!; - - public string StagingDirectory => $"{FinalDirectory}.staging"; - public virtual string ExecutableName { get; } = null!; + + public virtual string Directory { get; } = null!; - public string ExecutablePath => Path.Combine(FinalDirectory, ExecutableName); + public string LockFilePath => Path.Combine(Directory, "Bloxstrap.lock"); + + public string ExecutablePath => Path.Combine(Directory, ExecutableName); public virtual IReadOnlyDictionary PackageDirectoryMap { get; set; } diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs index 719dd9b..b8a45c9 100644 --- a/Bloxstrap/AppData/IAppData.cs +++ b/Bloxstrap/AppData/IAppData.cs @@ -12,9 +12,9 @@ string StartEvent { get; } - string FinalDirectory { get; } + string Directory { get; } - string StagingDirectory { get; } + string LockFilePath { get; } string ExecutablePath { get; } diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index 156d714..923c6a1 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -18,7 +18,7 @@ namespace Bloxstrap.AppData public string StartEvent => "www.roblox.com/robloxStartedEvent"; - public override string FinalDirectory => Path.Combine(Paths.Roblox, "Player"); + public override string Directory => Path.Combine(Paths.Roblox, "Player"); public AppState State => App.State.Prop.Player; diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 18b1cca..fca5a63 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -12,7 +12,7 @@ public string StartEvent => "www.roblox.com/robloxStudioStartedEvent"; - public override string FinalDirectory => Path.Combine(Paths.Roblox, "Studio"); + public override string Directory => Path.Combine(Paths.Roblox, "Studio"); public AppState State => App.State.Prop.Studio; diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 207fa9a..8c4b725 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -47,6 +47,9 @@ namespace Bloxstrap private double _progressIncrement; private long _totalDownloadedBytes = 0; + private bool _mustUpgrade => File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); + private bool _skipUpgrade = false; + public IBootstrapperDialog? Dialog = null; public bool IsStudioLaunch => _launchMode != LaunchMode.Player; @@ -162,8 +165,8 @@ namespace Bloxstrap await GetLatestVersionInfo(); // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (!File.Exists(AppData.ExecutablePath) || AppData.State.VersionGuid != _latestVersionGuid) - await InstallLatestVersion(); + if (!_skipUpgrade && (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)) + await UpgradeRoblox(); //await ApplyModifications(); @@ -284,7 +287,7 @@ namespace Bloxstrap { FileName = AppData.ExecutablePath, Arguments = _launchCommandLine, - WorkingDirectory = AppData.FinalDirectory + WorkingDirectory = AppData.Directory }; if (_launchMode == LaunchMode.StudioAuth) @@ -362,14 +365,17 @@ namespace Bloxstrap { using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5)); + // TODO: look into if this needs to be launched *before* roblox starts if (ipl.IsAcquired) Process.Start(Paths.Process, $"-watcher \"{args}\""); } } - public void CancelInstall() + // TODO: the bootstrapper dialogs call this function directly. + // this should probably be behind an event invocation. + public void Cancel() { - const string LOG_IDENT = "Bootstrapper::CancelInstall"; + const string LOG_IDENT = "Bootstrapper::Cancel"; if (!_isInstalling) { @@ -381,20 +387,23 @@ namespace Bloxstrap if (_cancelTokenSource.IsCancellationRequested) return; - App.Logger.WriteLine(LOG_IDENT, "Cancelling install..."); + App.Logger.WriteLine(LOG_IDENT, "Cancelling launch..."); _cancelTokenSource.Cancel(); - try + if (_isInstalling) { - // clean up install - if (Directory.Exists(AppData.StagingDirectory)) - Directory.Delete(AppData.StagingDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Could not fully clean up installation!"); - App.Logger.WriteException(LOG_IDENT, ex); + try + { + // clean up install + if (Directory.Exists(AppData.Directory)) + Directory.Delete(AppData.Directory, true); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, "Could not fully clean up installation!"); + App.Logger.WriteException(LOG_IDENT, ex); + } } Dialog?.CloseBootstrapper(); @@ -509,22 +518,46 @@ namespace Bloxstrap #endregion #region Roblox Install - private async Task InstallLatestVersion() + private async Task UpgradeRoblox() { - const string LOG_IDENT = "Bootstrapper::InstallLatestVersion"; + const string LOG_IDENT = "Bootstrapper::UpgradeRoblox"; - _isInstalling = true; - SetStatus(FreshInstall ? Strings.Bootstrapper_Status_Installing : Strings.Bootstrapper_Status_Upgrading); Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Downloads); Directory.CreateDirectory(Paths.Roblox); - if (Directory.Exists(AppData.StagingDirectory)) - Directory.Delete(AppData.StagingDirectory, true); + if (Directory.Exists(AppData.Directory)) + { + try + { + // gross hack to see if roblox is still running + // i don't want to rely on mutexes because they can change, and will false flag for + // running installations that are not by bloxstrap + File.Delete(AppData.ExecutablePath); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, "Could not delete executable/folder, Roblox may still be running. Aborting update."); + App.Logger.WriteException(LOG_IDENT, ex); - Directory.CreateDirectory(AppData.StagingDirectory); + Directory.Delete(AppData.Directory); + + return; + } + + Directory.Delete(AppData.Directory, true); + } + + _isInstalling = true; + + Directory.CreateDirectory(AppData.Directory); + + // installer lock, it should only be present while roblox is in the process of upgrading + // if it's present while we're launching, then it's an unfinished install and must be reinstalled + var lockFile = new FileInfo(AppData.LockFilePath); + lockFile.Create().Dispose(); // 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 @@ -564,8 +597,7 @@ namespace Bloxstrap if (package.Name == "WebView2RuntimeInstaller.zip") continue; - // extract the package immediately after download asynchronously - // discard is just used to suppress the warning + // extract the package async immediately after download extractionTasks.Add(Task.Run(() => ExtractPackage(package), _cancelTokenSource.Token)); } @@ -586,11 +618,13 @@ namespace Bloxstrap await Task.WhenAll(extractionTasks); App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); - await File.WriteAllTextAsync(Path.Combine(AppData.StagingDirectory, "AppSettings.xml"), AppSettings); + await File.WriteAllTextAsync(Path.Combine(AppData.Directory, "AppSettings.xml"), AppSettings); if (_cancelTokenSource.IsCancellationRequested) return; + // only prompt on fresh install, since we don't want to be prompting them for every single launch + // TODO: state entry? if (FreshInstall) { using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); @@ -612,7 +646,7 @@ namespace Bloxstrap return; } - string baseDirectory = Path.Combine(AppData.StagingDirectory, AppData.PackageDirectoryMap[package.Name]); + string baseDirectory = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); ExtractPackage(package); @@ -681,37 +715,13 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); + App.State.Save(); + + lockFile.Delete(); + if (Dialog is not null) Dialog.CancelEnabled = false; - if (Directory.Exists(AppData.FinalDirectory)) - { - try - { - // gross hack to see if roblox is still running - // i don't want to rely on mutexes because they can change, and will false flag for - // running installations that are not by bloxstrap - File.Delete(AppData.ExecutablePath); - - Directory.Delete(AppData.FinalDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Could not delete executable/folder, Roblox may still be running. Aborting update."); - App.Logger.WriteException(LOG_IDENT, ex); - - Directory.Delete(AppData.StagingDirectory); - - _isInstalling = false; - - return; - } - } - - Directory.Move(AppData.StagingDirectory, AppData.FinalDirectory); - - App.State.Save(); - _isInstalling = false; } @@ -996,7 +1006,7 @@ namespace Bloxstrap const string LOG_IDENT = "Bootstrapper::ExtractPackage"; string packageLocation = Path.Combine(Paths.Downloads, package.Signature); - string packageFolder = Path.Combine(AppData.StagingDirectory, AppData.PackageDirectoryMap[package.Name]); + string packageFolder = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); diff --git a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs index 588d950..33974f1 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs @@ -108,7 +108,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base public void Dialog_FormClosing(object sender, FormClosingEventArgs e) { if (!_isClosing) - Bootstrapper?.CancelInstall(); + Bootstrapper?.Cancel(); } #endregion diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs index 54e57ca..67575a8 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs @@ -105,7 +105,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper private void Window_Closing(object sender, CancelEventArgs e) { if (!_isClosing) - Bootstrapper?.CancelInstall(); + Bootstrapper?.Cancel(); } #region IBootstrapperDialog Methods diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml.cs index 5139a3c..a007e31 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml.cs @@ -88,7 +88,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper private void UiWindow_Closing(object sender, CancelEventArgs e) { if (!_isClosing) - Bootstrapper?.CancelInstall(); + Bootstrapper?.Cancel(); } #region IBootstrapperDialog Methods diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs index 9048f02..18a2871 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs @@ -102,7 +102,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper private void UiWindow_Closing(object sender, CancelEventArgs e) { if (!_isClosing) - Bootstrapper?.CancelInstall(); + Bootstrapper?.Cancel(); } #region IBootstrapperDialog Methods diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs index 27e21da..9d35dac 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs @@ -35,7 +35,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper private void CancelInstall() { - _dialog.Bootstrapper?.CancelInstall(); + _dialog.Bootstrapper?.Cancel(); _dialog.CloseBootstrapper(); } } From 1bdf761d075088e4b4862d30d4ba51dc54ca0fae Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 7 Sep 2024 23:11:39 +0100 Subject: [PATCH 05/30] Fix bug in cached package handling --- Bloxstrap/Bootstrapper.cs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 8c4b725..8b2c332 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -34,9 +34,9 @@ namespace Bloxstrap private readonly CancellationTokenSource _cancelTokenSource = new(); - private IAppData AppData; + private readonly IAppData AppData; - private bool FreshInstall => String.IsNullOrEmpty(AppData.State.VersionGuid); + private bool FreshInstall => !File.Exists(AppData.ExecutablePath) || String.IsNullOrEmpty(AppData.State.VersionGuid); private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; private LaunchMode _launchMode = App.LaunchSettings.RobloxLaunchMode; @@ -559,10 +559,14 @@ namespace Bloxstrap var lockFile = new FileInfo(AppData.LockFilePath); lockFile.Create().Dispose(); + var cachedPackageHashes = Directory.GetFiles(Paths.Downloads).Select(x => Path.GetFileName(x)); + // package manifest states packed size and uncompressed size in exact bytes + int totalSizeRequired = 0; + // packed size only matters if we don't already have the package cached on disk - var cachedPackages = Directory.GetFiles(Paths.Downloads); - int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); + totalSizeRequired += _versionPackageManifest.Where(x => !cachedPackageHashes.Contains(x.Signature)).Sum(x => x.PackedSize); + totalSizeRequired += _versionPackageManifest.Sum(x => x.Size); if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) { @@ -682,19 +686,19 @@ namespace Bloxstrap allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values); allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values); - foreach (string filename in cachedPackages) + foreach (string hash in cachedPackageHashes) { - if (!allPackageHashes.Contains(filename)) + if (!allPackageHashes.Contains(hash)) { - App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); + App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}"); try { - File.Delete(filename); + File.Delete(Path.Combine(Paths.Downloads, hash)); } catch (Exception ex) { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {filename}!"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!"); App.Logger.WriteException(LOG_IDENT, ex); } } @@ -702,7 +706,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Registering approximate program size..."); - int distributionSize = _versionPackageManifest.Sum(x => x.Size + x.PackedSize) / 1000; + int distributionSize = _versionPackageManifest.Sum(x => x.Size + x.PackedSize) / 1024; AppData.State.Size = distributionSize; From 2795ccf92ed91b0c92d5a94e6133c28ea04d19ef Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 8 Sep 2024 01:23:35 +0100 Subject: [PATCH 06/30] Bulk restoration of deleted mod files --- Bloxstrap/Bootstrapper.cs | 283 ++++++++++++++------------- Bloxstrap/Models/Manifest/Package.cs | 5 + 2 files changed, 152 insertions(+), 136 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 8b2c332..4ad89c3 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -48,7 +48,7 @@ namespace Bloxstrap private long _totalDownloadedBytes = 0; private bool _mustUpgrade => File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); - private bool _skipUpgrade = false; + private bool _noConnection = false; public IBootstrapperDialog? Dialog = null; @@ -165,10 +165,11 @@ namespace Bloxstrap await GetLatestVersionInfo(); // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (!_skipUpgrade && (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)) + if (!_noConnection && (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)) await UpgradeRoblox(); - //await ApplyModifications(); + if (!_noConnection) + await ApplyModifications(); // check if launch uri is set to our bootstrapper // this doesn't go under register, so we check every launch @@ -729,144 +730,164 @@ namespace Bloxstrap _isInstalling = false; } - //private async Task ApplyModifications() - //{ - // const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - - // if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) - // { - // App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); - // return; - // } + private async Task ApplyModifications() + { + const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - // SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); + SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); - // // handle file mods - // App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); + // handle file mods + App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); - // // manifest has been moved to State.json - // File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); + // manifest has been moved to State.json + File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); - // List modFolderFiles = new(); + List modFolderFiles = new(); - // if (!Directory.Exists(Paths.Modifications)) - // Directory.CreateDirectory(Paths.Modifications); + if (!Directory.Exists(Paths.Modifications)) + Directory.CreateDirectory(Paths.Modifications); - // // check custom font mod - // // instead of replacing the fonts themselves, we'll just alter the font family manifests + // check custom font mod + // instead of replacing the fonts themselves, we'll just alter the font family manifests - // string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); + string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - // if (File.Exists(Paths.CustomFont)) - // { - // App.Logger.WriteLine(LOG_IDENT, "Begin font check"); + if (File.Exists(Paths.CustomFont)) + { + App.Logger.WriteLine(LOG_IDENT, "Begin font check"); - // Directory.CreateDirectory(modFontFamiliesFolder); + 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); + const string path = "rbxasset://fonts/CustomFont.ttf"; - // if (File.Exists(modFilepath)) - // continue; + foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(AppData.Directory, "content\\fonts\\families"))) + { + string jsonFilename = Path.GetFileName(jsonFilePath); + string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); - // App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); + if (File.Exists(modFilepath)) + continue; - // FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); + App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); - // if (fontFamilyData is null) - // continue; + var fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); - // foreach (FontFace fontFace in fontFamilyData.Faces) - // fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; + if (fontFamilyData is null) + continue; - // // TODO: writing on every launch is not necessary - // File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); - // } + bool shouldWrite = false; - // App.Logger.WriteLine(LOG_IDENT, "End font check"); - // } - // else if (Directory.Exists(modFontFamiliesFolder)) - // { - // Directory.Delete(modFontFamiliesFolder, true); - // } + foreach (var fontFace in fontFamilyData.Faces) + { + if (fontFace.AssetId != path) + { + fontFace.AssetId = path; + shouldWrite = true; + } + } - // foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) - // { - // // get relative directory path - // string relativeFile = file.Substring(Paths.Modifications.Length + 1); + if (shouldWrite) + File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); + } - // // v1.7.0 - README has been moved to the preferences menu now - // if (relativeFile == "README.txt") - // { - // File.Delete(file); - // continue; - // } + App.Logger.WriteLine(LOG_IDENT, "End font check"); + } + else if (Directory.Exists(modFontFamiliesFolder)) + { + Directory.Delete(modFontFamiliesFolder, true); + } - // if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) - // continue; + foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) + { + // get relative directory path + string relativeFile = file.Substring(Paths.Modifications.Length + 1); - // if (relativeFile.EndsWith(".lock")) - // continue; + // v1.7.0 - README has been moved to the preferences menu now + if (relativeFile == "README.txt") + { + File.Delete(file); + continue; + } - // modFolderFiles.Add(relativeFile); + if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) + continue; - // string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); - // string fileVersionFolder = Path.Combine(_versionFolder, relativeFile); + if (relativeFile.EndsWith(".lock")) + continue; - // if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) - // { - // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); - // continue; - // } + modFolderFiles.Add(relativeFile); - // Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); + string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); + string fileVersionFolder = Path.Combine(AppData.Directory, relativeFile); - // Filesystem.AssertReadOnly(fileVersionFolder); - // File.Copy(fileModFolder, fileVersionFolder, true); - // Filesystem.AssertReadOnly(fileVersionFolder); + if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) + { + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); + continue; + } - // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); - // } + Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); - // // 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 + Filesystem.AssertReadOnly(fileVersionFolder); + File.Copy(fileModFolder, fileVersionFolder, true); + Filesystem.AssertReadOnly(fileVersionFolder); - // // TODO: this needs to extract the files from packages in bulk, this is way too slow - // foreach (string fileLocation in App.State.Prop.ModManifest) - // { - // if (modFolderFiles.Contains(fileLocation)) - // continue; + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); + } - // var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + // 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 - // // package doesn't exist, likely mistakenly placed file - // if (String.IsNullOrEmpty(package.Key)) - // { - // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); + var fileRestoreMap = new Dictionary>(); - // string versionFileLocation = Path.Combine(_versionFolder, fileLocation); + foreach (string fileLocation in App.State.Prop.ModManifest) + { + if (modFolderFiles.Contains(fileLocation)) + continue; - // if (File.Exists(versionFileLocation)) - // File.Delete(versionFileLocation); + var packageMapEntry = AppData.PackageDirectoryMap.SingleOrDefault(x => !String.IsNullOrEmpty(x.Value) && fileLocation.StartsWith(x.Value)); + string packageName = packageMapEntry.Key; - // continue; - // } + // package doesn't exist, likely mistakenly placed file + if (String.IsNullOrEmpty(packageName)) + { + App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); - // // restore original file - // string fileName = fileLocation.Substring(package.Value.Length); - // await ExtractFileFromPackage(package.Key, fileName); + string versionFileLocation = Path.Combine(AppData.Directory, fileLocation); - // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}"); - // } + if (File.Exists(versionFileLocation)) + File.Delete(versionFileLocation); - // App.State.Prop.ModManifest = modFolderFiles; - // App.State.Save(); + continue; + } - // App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); - //} + string fileName = fileLocation.Substring(packageMapEntry.Value.Length); + + if (!fileRestoreMap.ContainsKey(packageName)) + fileRestoreMap[packageName] = new(); + + fileRestoreMap[packageName].Add(fileName); + + App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restoring from {packageName}"); + } + + foreach (var entry in fileRestoreMap) + { + var package = _versionPackageManifest.Find(x => x.Name == entry.Key); + + if (package is not null) + { + await DownloadPackage(package); + ExtractPackage(package, entry.Value); + } + } + + App.State.Prop.ModManifest = modFolderFiles; + App.State.Save(); + + App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); + } private async Task DownloadPackage(Package package) { @@ -876,14 +897,13 @@ namespace Bloxstrap return; string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string packageLocation = Path.Combine(Paths.Downloads, package.Signature); string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature); - if (File.Exists(packageLocation)) + if (File.Exists(package.DownloadPath)) { - var file = new FileInfo(packageLocation); + var file = new FileInfo(package.DownloadPath); - string calculatedMD5 = MD5Hash.FromFile(packageLocation); + string calculatedMD5 = MD5Hash.FromFile(package.DownloadPath); if (calculatedMD5 != package.Signature) { @@ -906,7 +926,7 @@ namespace Bloxstrap // then we can just copy the one from there App.Logger.WriteLine(LOG_IDENT, $"Found existing copy at '{robloxPackageLocation}'! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, packageLocation); + File.Copy(robloxPackageLocation, package.DownloadPath); _totalDownloadedBytes += package.PackedSize; UpdateProgressBar(); @@ -914,7 +934,7 @@ namespace Bloxstrap return; } - if (File.Exists(packageLocation)) + if (File.Exists(package.DownloadPath)) return; // TODO: telemetry for this. chances are that this is completely unnecessary and that it can be removed. @@ -937,7 +957,7 @@ namespace Bloxstrap { var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); + await using var fileStream = new FileStream(package.DownloadPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); while (true) { @@ -987,8 +1007,8 @@ namespace Bloxstrap else if (i >= maxTries) throw; - if (File.Exists(packageLocation)) - File.Delete(packageLocation); + if (File.Exists(package.DownloadPath)) + File.Delete(package.DownloadPath); _totalDownloadedBytes -= totalBytesRead; UpdateProgressBar(); @@ -1005,40 +1025,31 @@ namespace Bloxstrap } } - private void ExtractPackage(Package package) + private void ExtractPackage(Package package, List? files = null) { const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - string packageLocation = Path.Combine(Paths.Downloads, package.Signature); string packageFolder = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); + string? fileFilter = null; + + // for sharpziplib, each file in the filter + if (files is not null) + { + var regexList = new List(); + + foreach (string file in files) + regexList.Add("^" + file.Replace("\\", "\\\\") + "$"); + + fileFilter = String.Join(';', regexList); + } App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip(); - fastZip.ExtractZip(packageLocation, packageFolder, null); + fastZip.ExtractZip(package.DownloadPath, packageFolder, fileFilter); App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); } - - //private async Task ExtractFileFromPackage(string packageName, string fileName) - //{ - // Package? package = _versionPackageManifest.Find(x => x.Name == packageName); - - // if (package is null) - // return; - - // await DownloadPackage(package); - - // using ZipArchive archive = ZipFile.OpenRead(Path.Combine(Paths.Downloads, package.Signature)); - - // ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); - - // if (entry is null) - // return; - - // string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName); - // entry.ExtractToFile(extractionPath, true); - //} -#endregion + #endregion } } diff --git a/Bloxstrap/Models/Manifest/Package.cs b/Bloxstrap/Models/Manifest/Package.cs index 6feb162..fe66d81 100644 --- a/Bloxstrap/Models/Manifest/Package.cs +++ b/Bloxstrap/Models/Manifest/Package.cs @@ -9,10 +9,15 @@ namespace Bloxstrap.Models.Manifest public class Package { public string Name { get; set; } = ""; + public string Signature { get; set; } = ""; + public int PackedSize { get; set; } + public int Size { get; set; } + public string DownloadPath => Path.Combine(Paths.Downloads, Signature); + public override string ToString() { return $"[{Signature}] {Name}"; From cd411e33ff97ecec72624ce79d303a8cb66a9ab7 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 8 Sep 2024 01:42:21 +0100 Subject: [PATCH 07/30] Use state entry for WebView2 runtime prompt --- Bloxstrap/Bootstrapper.cs | 40 +++++++++++++++++++++++++-------------- Bloxstrap/Models/State.cs | 2 ++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 4ad89c3..8beaea4 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -36,8 +36,6 @@ namespace Bloxstrap private readonly IAppData AppData; - private bool FreshInstall => !File.Exists(AppData.ExecutablePath) || String.IsNullOrEmpty(AppData.State.VersionGuid); - private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; private LaunchMode _launchMode = App.LaunchSettings.RobloxLaunchMode; private string _latestVersionGuid = null!; @@ -47,7 +45,7 @@ namespace Bloxstrap private double _progressIncrement; private long _totalDownloadedBytes = 0; - private bool _mustUpgrade => File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); + private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); private bool _noConnection = false; public IBootstrapperDialog? Dialog = null; @@ -164,12 +162,16 @@ namespace Bloxstrap // TODO: handle exception and update skip await GetLatestVersionInfo(); - // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (!_noConnection && (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)) - await UpgradeRoblox(); - if (!_noConnection) + { + if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) + await UpgradeRoblox(); + + // we require deployment details for applying modifications for a worst case scenario, + // where we'd need to restore files from a package that isn't present on disk and needs to be redownloaded await ApplyModifications(); + } + // check if launch uri is set to our bootstrapper // this doesn't go under register, so we check every launch @@ -522,8 +524,11 @@ namespace Bloxstrap private async Task UpgradeRoblox() { const string LOG_IDENT = "Bootstrapper::UpgradeRoblox"; - - SetStatus(FreshInstall ? Strings.Bootstrapper_Status_Installing : Strings.Bootstrapper_Status_Upgrading); + + if (String.IsNullOrEmpty(AppData.State.VersionGuid)) + SetStatus(Strings.Bootstrapper_Status_Installing); + else + SetStatus(Strings.Bootstrapper_Status_Upgrading); Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Downloads); @@ -628,18 +633,25 @@ namespace Bloxstrap if (_cancelTokenSource.IsCancellationRequested) return; - // only prompt on fresh install, since we don't want to be prompting them for every single launch - // TODO: state entry? - if (FreshInstall) + if (App.State.Prop.PromptWebView2Install) { using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - if (hklmKey is null && hkcuKey is null) + if (hklmKey is not null || hkcuKey is not null) + { + // reset prompt state if the user has it installed + App.State.Prop.PromptWebView2Install = true; + } + else { var result = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes); - if (result == MessageBoxResult.Yes) + if (result != MessageBoxResult.Yes) + { + App.State.Prop.PromptWebView2Install = false; + } + else { App.Logger.WriteLine(LOG_IDENT, "Installing WebView2 runtime..."); diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index 9aeea4a..45e2ef4 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -3,6 +3,8 @@ public class State { public bool ShowFFlagEditorWarning { get; set; } = true; + + public bool PromptWebView2Install { get; set; } = true; public AppState Player { get; set; } = new(); From dd568faf9c6ecacf5f0b39847bcc37a0498d7d8a Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 8 Sep 2024 23:59:38 +0100 Subject: [PATCH 08/30] Implement new bootstrapper error handling --- Bloxstrap/App.xaml.cs | 3 +- Bloxstrap/Bootstrapper.cs | 71 ++++++++------ Bloxstrap/Exceptions/HttpResponseException.cs | 19 ---- Bloxstrap/Resources/Strings.Designer.cs | 92 +++++++++++++------ Bloxstrap/Resources/Strings.resx | 26 ++++-- Bloxstrap/RobloxDeployment.cs | 7 +- Bloxstrap/RobloxFastFlags.cs | 11 +-- .../Elements/Dialogs/ConnectivityDialog.xaml | 3 +- .../Dialogs/ConnectivityDialog.xaml.cs | 37 +++++++- Bloxstrap/UI/Frontend.cs | 10 +- 10 files changed, 178 insertions(+), 101 deletions(-) delete mode 100644 Bloxstrap/Exceptions/HttpResponseException.cs diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 858b85e..bc333f0 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -101,8 +101,7 @@ namespace Bloxstrap _showingExceptionDialog = true; - if (!LaunchSettings.QuietFlag.Active) - Frontend.ShowExceptionDialog(ex); + Frontend.ShowExceptionDialog(ex); Terminate(ErrorCode.ERROR_INSTALL_FAILURE); } diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 8beaea4..fb23a26 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -82,6 +82,35 @@ namespace Bloxstrap Dialog.ProgressValue = progressValue; } + + private void HandleConnectionError(Exception exception) + { + _noConnection = true; + + string message = Strings.Dialog_Connectivity_Preventing; + + if (exception.GetType() == typeof(AggregateException)) + exception = exception.InnerException!; + + if (exception.GetType() == typeof(HttpRequestException)) + message = String.Format(Strings.Dialog_Connectivity_RobloxDown, "[status.roblox.com](https://status.roblox.com)"); + else if (exception.GetType() == typeof(TaskCanceledException)) + message = Strings.Dialog_Connectivity_TimedOut; + + if (_mustUpgrade) + message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeNeeded}\n\n{Strings.Dialog_Connectivity_TryAgainLater}"; + else + message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeSkip}"; + + Frontend.ShowConnectivityDialog( + String.Format(Strings.Dialog_Connectivity_UnableToConnect, "Roblox"), + message, + _mustUpgrade ? MessageBoxImage.Error : MessageBoxImage.Warning, + exception); + + if (_mustUpgrade) + App.Terminate(ErrorCode.ERROR_CANCELLED); + } public async Task Run() { @@ -101,24 +130,7 @@ namespace Bloxstrap if (connectionResult is not null) { - App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed!"); - App.Logger.WriteException(LOG_IDENT, connectionResult); - - string message = Strings.Bootstrapper_Connectivity_Preventing; - - if (connectionResult.GetType() == typeof(HttpResponseException)) - message = Strings.Bootstrapper_Connectivity_RobloxDown; - else if (connectionResult.GetType() == typeof(TaskCanceledException)) - message = Strings.Bootstrapper_Connectivity_TimedOut; - else if (connectionResult.GetType() == typeof(AggregateException)) - connectionResult = connectionResult.InnerException!; - - // TODO: handle update skip - Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, connectionResult); - - App.Terminate(ErrorCode.ERROR_CANCELLED); - - return; + HandleConnectionError(connectionResult); } #if !DEBUG || DEBUG_UPDATER @@ -159,8 +171,17 @@ namespace Bloxstrap App.State.Load(); } - // TODO: handle exception and update skip - await GetLatestVersionInfo(); + if (!_noConnection) + { + try + { + await GetLatestVersionInfo(); + } + catch (Exception ex) + { + HandleConnectionError(ex); + } + } if (!_noConnection) { @@ -172,7 +193,6 @@ namespace Bloxstrap await ApplyModifications(); } - // 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 @@ -232,15 +252,14 @@ namespace Bloxstrap { clientVersion = await RobloxDeployment.GetInfo(channel, AppData.BinaryType); } - catch (HttpResponseException ex) + catch (HttpRequestException ex) { - if (ex.ResponseMessage.StatusCode - is not HttpStatusCode.Unauthorized + if (ex.StatusCode is not HttpStatusCode.Unauthorized and not HttpStatusCode.Forbidden and not HttpStatusCode.NotFound) throw; - App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {channel} to {RobloxDeployment.DefaultChannel} because HTTP {(int)ex.ResponseMessage.StatusCode}"); + App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {channel} to {RobloxDeployment.DefaultChannel} because HTTP {(int)ex.StatusCode}"); channel = RobloxDeployment.DefaultChannel; clientVersion = await RobloxDeployment.GetInfo(channel, AppData.BinaryType); @@ -624,7 +643,6 @@ namespace Bloxstrap SetStatus(Strings.Bootstrapper_Status_Configuring); } - // TODO: handle faulted tasks await Task.WhenAll(extractionTasks); App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); @@ -1011,6 +1029,7 @@ namespace Bloxstrap Frontend.ShowConnectivityDialog( Strings.Dialog_Connectivity_UnableToDownload, String.Format(Strings.Dialog_Connectivity_UnableToDownloadReason, "[https://github.com/pizzaboxer/bloxstrap/wiki/Bloxstrap-is-unable-to-download-Roblox](https://github.com/pizzaboxer/bloxstrap/wiki/Bloxstrap-is-unable-to-download-Roblox)"), + MessageBoxImage.Error, ex ); diff --git a/Bloxstrap/Exceptions/HttpResponseException.cs b/Bloxstrap/Exceptions/HttpResponseException.cs deleted file mode 100644 index 08404b0..0000000 --- a/Bloxstrap/Exceptions/HttpResponseException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Exceptions -{ - internal class HttpResponseException : Exception - { - public HttpResponseMessage ResponseMessage { get; } - - public HttpResponseException(HttpResponseMessage responseMessage) - : base($"Could not connect to {responseMessage.RequestMessage!.RequestUri} because it returned HTTP {(int)responseMessage.StatusCode} ({responseMessage.ReasonPhrase})") - { - ResponseMessage = responseMessage; - } - } -} diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 0d863f5..981861c 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -141,33 +141,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to It's possible that something is preventing Bloxstrap from connecting to the internet. Please check and try again.. - /// - public static string Bootstrapper_Connectivity_Preventing { - get { - return ResourceManager.GetString("Bootstrapper.Connectivity.Preventing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Roblox may be down right now. See status.roblox.com for more information. Please try again later.. - /// - public static string Bootstrapper_Connectivity_RobloxDown { - get { - return ResourceManager.GetString("Bootstrapper.Connectivity.RobloxDown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bloxstrap timed out when trying to connect to three different Roblox deployment mirrors, indicating a poor internet connection. Please try again later.. - /// - public static string Bootstrapper_Connectivity_TimedOut { - get { - return ResourceManager.GetString("Bootstrapper.Connectivity.TimedOut", resourceCulture); - } - } - /// /// Looks up a localized string similar to Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu.. /// @@ -828,6 +801,60 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Something is likely preventing Bloxstrap from connecting to the internet.. + /// + public static string Dialog_Connectivity_Preventing { + get { + return ResourceManager.GetString("Dialog.Connectivity.Preventing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Roblox may be down right now. See {0} for more information.. + /// + public static string Dialog_Connectivity_RobloxDown { + get { + return ResourceManager.GetString("Dialog.Connectivity.RobloxDown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue.. + /// + public static string Dialog_Connectivity_RobloxUpgradeNeeded { + get { + return ResourceManager.GetString("Dialog.Connectivity.RobloxUpgradeNeeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For this launch, Roblox will not be checked for upgrades, and changes to mods will not be applied.. + /// + public static string Dialog_Connectivity_RobloxUpgradeSkip { + get { + return ResourceManager.GetString("Dialog.Connectivity.RobloxUpgradeSkip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} may be down right now.. + /// + public static string Dialog_Connectivity_ServiceDown { + get { + return ResourceManager.GetString("Dialog.Connectivity.ServiceDown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The connection timed out, which could indicate a poor internet connection or a firewall block.. + /// + public static string Dialog_Connectivity_TimedOut { + get { + return ResourceManager.GetString("Dialog.Connectivity.TimedOut", resourceCulture); + } + } + /// /// Looks up a localized string similar to Connectivity error. /// @@ -838,7 +865,16 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Bloxstrap is unable to connect to Roblox. + /// Looks up a localized string similar to Please try again later.. + /// + public static string Dialog_Connectivity_TryAgainLater { + get { + return ResourceManager.GetString("Dialog.Connectivity.TryAgainLater", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bloxstrap is unable to connect to {0}. /// public static string Dialog_Connectivity_UnableToConnect { get { diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 3301be8..890ec70 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -123,14 +123,14 @@ Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching? - - It's possible that something is preventing Bloxstrap from connecting to the internet. Please check and try again. + + Something is likely preventing Bloxstrap from connecting to the internet. - - Roblox may be down right now. See status.roblox.com for more information. Please try again later. + + Roblox may be down right now. See {0} for more information. - - Bloxstrap timed out when trying to connect to three different Roblox deployment mirrors, indicating a poor internet connection. Please try again later. + + The connection timed out, which could indicate a poor internet connection or a firewall block. Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu. @@ -294,7 +294,7 @@ Click for more information Connectivity error - Bloxstrap is unable to connect to Roblox + Bloxstrap is unable to connect to {0} Copy log contents @@ -1171,4 +1171,16 @@ Are you sure you want to continue? Failed to query server location. + + {0} may be down right now. + + + Please try again later. + + + For this launch, Roblox will not be checked for upgrades, and changes to mods will not be applied. + + + Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue. + \ No newline at end of file diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index d23a5aa..e2022c1 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -33,8 +33,7 @@ { var response = await App.HttpClient.GetAsync($"{url}/versionStudio", token); - if (!response.IsSuccessStatusCode) - throw new HttpResponseException(response); + response.EnsureSuccessStatusCode(); // versionStudio is the version hash for the last MFC studio to be deployed. // the response body should always be "version-012732894899482c". @@ -151,14 +150,14 @@ try { - clientVersion = await Http.GetJson($"https://clientsettingscdn.roblox.com/{path}"); + clientVersion = await Http.GetJson("https://clientsettingscdn.roblox.com" + path); } catch (Exception ex) { App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings..."); App.Logger.WriteException(LOG_IDENT, ex); - clientVersion = await Http.GetJson($"https://clientsettings.roblox.com/{path}"); + clientVersion = await Http.GetJson("https://clientsettings.roblox.com" + path); } // check if channel is behind LIVE diff --git a/Bloxstrap/RobloxFastFlags.cs b/Bloxstrap/RobloxFastFlags.cs index 04e45da..38799c1 100644 --- a/Bloxstrap/RobloxFastFlags.cs +++ b/Bloxstrap/RobloxFastFlags.cs @@ -52,16 +52,7 @@ namespace Bloxstrap string rawResponse = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) - { - App.Logger.WriteLine(logIndent, - "Failed to fetch client settings!\r\n" + - $"\tStatus code: {response.StatusCode}\r\n" + - $"\tResponse: {rawResponse}" - ); - - throw new HttpResponseException(response); - } + response.EnsureSuccessStatusCode(); var clientSettings = JsonSerializer.Deserialize(rawResponse); diff --git a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml index 5ad5299..ca2f96b 100644 --- a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml +++ b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml @@ -12,6 +12,7 @@ Width="480" MinHeight="0" SizeToContent="Height" + Title="{x:Static resources:Strings.Dialog_Connectivity_Title}" Background="{ui:ThemeResource ApplicationBackgroundBrush}" ExtendsContentIntoTitleBar="True" WindowStartupLocation="CenterScreen"> @@ -29,7 +30,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml.cs index 21f31bb..494ee69 100644 --- a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml.cs @@ -1,5 +1,7 @@ using System.Media; +using System.Windows; using System.Windows.Interop; +using System.Windows.Media.Imaging; using Windows.Win32; using Windows.Win32.Foundation; @@ -14,10 +16,41 @@ namespace Bloxstrap.UI.Elements.Dialogs /// public partial class ConnectivityDialog { - public ConnectivityDialog(string title, string description, Exception exception) + public ConnectivityDialog(string title, string description, MessageBoxImage image, Exception exception) { InitializeComponent(); + string? iconFilename = null; + SystemSound? sound = null; + + switch (image) + { + case MessageBoxImage.Error: + iconFilename = "Error"; + sound = SystemSounds.Hand; + break; + + case MessageBoxImage.Question: + iconFilename = "Question"; + sound = SystemSounds.Question; + break; + + case MessageBoxImage.Warning: + iconFilename = "Warning"; + sound = SystemSounds.Exclamation; + break; + + case MessageBoxImage.Information: + iconFilename = "Information"; + sound = SystemSounds.Asterisk; + break; + } + + if (iconFilename is null) + IconImage.Visibility = Visibility.Collapsed; + else + IconImage.Source = new BitmapImage(new Uri($"pack://application:,,,/Resources/MessageBox/{iconFilename}.png")); + TitleTextBlock.Text = title; DescriptionTextBlock.MarkdownText = description; @@ -28,7 +61,7 @@ namespace Bloxstrap.UI.Elements.Dialogs Close(); }; - SystemSounds.Hand.Play(); + sound?.Play(); Loaded += delegate { diff --git a/Bloxstrap/UI/Frontend.cs b/Bloxstrap/UI/Frontend.cs index 1699b6b..f1dfab2 100644 --- a/Bloxstrap/UI/Frontend.cs +++ b/Bloxstrap/UI/Frontend.cs @@ -49,17 +49,23 @@ namespace Bloxstrap.UI public static void ShowExceptionDialog(Exception exception) { + if (App.LaunchSettings.QuietFlag.Active) + return; + Application.Current.Dispatcher.Invoke(() => { new ExceptionDialog(exception).ShowDialog(); }); } - public static void ShowConnectivityDialog(string title, string description, Exception exception) + public static void ShowConnectivityDialog(string title, string description, MessageBoxImage image, Exception exception) { + if (App.LaunchSettings.QuietFlag.Active) + return; + Application.Current.Dispatcher.Invoke(() => { - new ConnectivityDialog(title, description, exception).ShowDialog(); + new ConnectivityDialog(title, description, image, exception).ShowDialog(); }); } From 62064117b6243daff96be3da8009bc718c2ba1a0 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 9 Sep 2024 11:41:21 +0100 Subject: [PATCH 09/30] Consolidate registry handling --- Bloxstrap/Bootstrapper.cs | 21 +----- Bloxstrap/Installer.cs | 49 ++++++-------- .../WindowsRegistry.cs} | 65 +++++++++++++++---- 3 files changed, 78 insertions(+), 57 deletions(-) rename Bloxstrap/{ProtocolHandler.cs => Utility/WindowsRegistry.cs} (52%) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index fb23a26..ee89778 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -193,27 +193,12 @@ namespace Bloxstrap await ApplyModifications(); } - // 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 + // check registry entries for every launch, just in case the stock bootstrapper changes it back if (IsStudioLaunch) - { -#if STUDIO_FEATURES - ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application); - ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application); - - ProtocolHandler.RegisterRobloxPlace(Paths.Application); - ProtocolHandler.RegisterExtension(".rbxl"); - ProtocolHandler.RegisterExtension(".rbxlx"); -#endif - } + WindowsRegistry.RegisterStudio(); else - { - // TODO: there needs to be better helper functions for these - ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); - ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\""); - } + WindowsRegistry.RegisterPlayer(); await mutex.ReleaseAsync(); diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index e807455..c67672a 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -63,10 +63,7 @@ namespace Bloxstrap // only register player, for the scenario where the user installs bloxstrap, closes it, // and then launches from the website expecting it to work // studio can be implicitly registered when it's first launched manually - ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); - ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\""); - - // TODO: implicit installation needs to reregister studio + WindowsRegistry.RegisterPlayer(); if (CreateDesktopShortcuts) Shortcut.Create(Paths.Application, "", DesktopShortcut); @@ -79,6 +76,9 @@ namespace Bloxstrap App.State.Load(false); App.FastFlags.Load(false); + if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid)) + WindowsRegistry.RegisterStudio(); + App.Logger.WriteLine(LOG_IDENT, "Installation finished"); } @@ -207,44 +207,38 @@ namespace Bloxstrap { playerStillInstalled = false; - ProtocolHandler.Unregister("roblox"); - ProtocolHandler.Unregister("roblox-player"); + WindowsRegistry.Unregister("roblox"); + WindowsRegistry.Unregister("roblox-player"); } else { - // revert launch uri handler to stock bootstrapper string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe"); - ProtocolHandler.Register("roblox", "Roblox", playerPath); - ProtocolHandler.Register("roblox-player", "Roblox", playerPath); + WindowsRegistry.RegisterPlayer(playerPath, "%1"); } - using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); - if (studioBootstrapperKey is null) + using var studioKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); + var studioFolder = studioKey?.GetValue("InstallLocation"); + + if (studioKey is null || studioFolder is not string) { studioStillInstalled = false; -#if STUDIO_FEATURES - ProtocolHandler.Unregister("roblox-studio"); - ProtocolHandler.Unregister("roblox-studio-auth"); + WindowsRegistry.Unregister("roblox-studio"); + WindowsRegistry.Unregister("roblox-studio-auth"); - ProtocolHandler.Unregister("Roblox.Place"); - ProtocolHandler.Unregister(".rbxl"); - ProtocolHandler.Unregister(".rbxlx"); -#endif + WindowsRegistry.Unregister("Roblox.Place"); + WindowsRegistry.Unregister(".rbxl"); + WindowsRegistry.Unregister(".rbxlx"); } -#if STUDIO_FEATURES else { - string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper - ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation); - ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation); + string studioPath = Path.Combine((string)studioFolder, "RobloxStudioBeta.exe"); + string studioLauncherPath = Path.Combine((string)studioFolder, "RobloxStudioLauncherBeta.exe"); - ProtocolHandler.RegisterRobloxPlace(studioLocation); + WindowsRegistry.RegisterStudioProtocol(studioPath, "%1"); + WindowsRegistry.RegisterStudioFileClass(studioPath, "-ide \"%1\""); } -#endif - - var cleanupSequence = new List { @@ -512,8 +506,7 @@ namespace Bloxstrap Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false); - ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); - ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\""); + WindowsRegistry.RegisterPlayer(); string? oldV2Val = App.FastFlags.GetValue("FFlagDisableNewIGMinDUA"); diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/Utility/WindowsRegistry.cs similarity index 52% rename from Bloxstrap/ProtocolHandler.cs rename to Bloxstrap/Utility/WindowsRegistry.cs index ff57e2c..be2981c 100644 --- a/Bloxstrap/ProtocolHandler.cs +++ b/Bloxstrap/Utility/WindowsRegistry.cs @@ -1,18 +1,15 @@ -using System.Web; -using System.Windows; +using Microsoft.Win32; -using Microsoft.Win32; - -namespace Bloxstrap +namespace Bloxstrap.Utility { - static class ProtocolHandler + static class WindowsRegistry { private const string RobloxPlaceKey = "Roblox.Place"; - public static void Register(string key, string name, string handler, string handlerParam = "%1") + public static void RegisterProtocol(string key, string name, string handler, string handlerParam = "%1") { string handlerArgs = $"\"{handler}\" {handlerParam}"; - + using var uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); using var uriIconKey = uriKey.CreateSubKey("DefaultIcon"); using var uriCommandKey = uriKey.CreateSubKey(@"shell\open\command"); @@ -30,10 +27,56 @@ namespace Bloxstrap } } - public static void RegisterRobloxPlace(string handler) + /// + /// Registers Roblox Player protocols for Bloxstrap + /// + public static void RegisterPlayer() => RegisterPlayer(Paths.Application, "-player \"%1\""); + + public static void RegisterPlayer(string handler, string handlerParam) + { + RegisterProtocol("roblox", "Roblox", handler, handlerParam); + RegisterProtocol("roblox-player", "Roblox", handler, handlerParam); + } + + /// + /// Registers all Roblox Studio classes for Bloxstrap + /// + public static void RegisterStudio() + { + RegisterStudioProtocol(Paths.Application, "-studio \"%1\""); + RegisterStudioFileClass(Paths.Application, "-studio \"%1\""); + RegisterStudioFileTypes(); + } + + /// + /// Registers roblox-studio and roblox-studio-auth protocols + /// + /// + /// + public static void RegisterStudioProtocol(string handler, string handlerParam) + { + RegisterProtocol("roblox-studio", "Roblox", handler, handlerParam); + RegisterProtocol("roblox-studio-auth", "Roblox", handler, handlerParam); + } + + /// + /// Registers file associations for Roblox.Place class + /// + public static void RegisterStudioFileTypes() + { + RegisterStudioFileType(".rbxl"); + RegisterStudioFileType(".rbxlx"); + } + + /// + /// Registers Roblox.Place class + /// + /// + /// + public static void RegisterStudioFileClass(string handler, string handlerParam) { const string keyValue = "Roblox Place"; - string handlerArgs = $"\"{handler}\" -ide \"%1\""; + string handlerArgs = $"\"{handler}\" {handlerParam}"; string iconValue = $"{handler},0"; using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey(@"Software\Classes\" + RobloxPlaceKey); @@ -54,7 +97,7 @@ namespace Bloxstrap uriIconKey.SetValue("", iconValue); } - public static void RegisterExtension(string key) + public static void RegisterStudioFileType(string key) { using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); uriKey.CreateSubKey(RobloxPlaceKey + @"\ShellNew"); From 356155c75b9c32ecd4aa9318b8f1abeb35b79a9c Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 9 Sep 2024 12:09:44 +0100 Subject: [PATCH 10/30] Fix potential bug in install checker UB would occur if installer.CheckInstallLocation() failed when attempting to change the install location --- Bloxstrap/App.xaml.cs | 37 ++++++++++++++++++++----------------- Bloxstrap/LaunchSettings.cs | 4 ++++ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index bc333f0..e8a91e2 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -198,6 +198,26 @@ namespace Bloxstrap } } + if (fixInstallLocation && installLocation is not null) + { + var installer = new Installer + { + InstallLocation = installLocation, + IsImplicitInstall = true + }; + + if (installer.CheckInstallLocation()) + { + Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'"); + installer.DoInstall(); + } + else + { + // force reinstall + installLocation = null; + } + } + if (installLocation is null) { Logger.Initialize(true); @@ -205,21 +225,6 @@ namespace Bloxstrap } else { - if (fixInstallLocation) - { - var installer = new Installer - { - InstallLocation = installLocation, - IsImplicitInstall = true - }; - - if (installer.CheckInstallLocation()) - { - Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'"); - installer.DoInstall(); - } - } - Paths.Initialize(installLocation); // ensure executable is in the install directory @@ -246,10 +251,8 @@ namespace Bloxstrap Locale.Set(Settings.Prop.Locale); -#if !DEBUG if (!LaunchSettings.BypassUpdateCheck) Installer.HandleUpgrade(); -#endif LaunchHandler.ProcessLaunchArgs(); } diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index 25a24fa..05ba45d 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -28,7 +28,11 @@ namespace Bloxstrap public LaunchFlag StudioFlag { get; } = new("studio"); +#if DEBUG + public bool BypassUpdateCheck => true; +#else public bool BypassUpdateCheck => UninstallFlag.Active || WatcherFlag.Active; +#endif public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None; From 7a0f50bd8160b1d207f4d85e468ee52421077f76 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 9 Sep 2024 12:40:44 +0100 Subject: [PATCH 11/30] Remove flag preset for graphics quality slider RIP --- Bloxstrap/FastFlagManager.cs | 2 -- Bloxstrap/Installer.cs | 2 ++ Bloxstrap/Resources/Strings.Designer.cs | 18 ------------------ Bloxstrap/Resources/Strings.resx | 6 ------ .../Elements/Settings/Pages/FastFlagsPage.xaml | 6 ------ .../ViewModels/Settings/FastFlagsViewModel.cs | 6 ------ 6 files changed, 2 insertions(+), 38 deletions(-) diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 5c92751..a444b80 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -49,7 +49,6 @@ namespace Bloxstrap { "UI.FlagState", "FStringDebugShowFlagState" }, #endif - { "UI.Menu.GraphicsSlider", "FFlagFixGraphicsQuality" }, { "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" }, { "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" }, @@ -62,7 +61,6 @@ namespace Bloxstrap { "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" } }; - // only one missing here is Metal because lol public static IReadOnlyDictionary RenderingModes => new Dictionary { { RenderingMode.Default, "None" }, diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index c67672a..2aff052 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -519,6 +519,8 @@ namespace Bloxstrap App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null); } + + App.FastFlags.SetValue("FFlagFixGraphicsQuality", null); } App.Settings.Save(); diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 981861c..67ab3a1 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -2306,24 +2306,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to Allows you to configure 21 different quality levels instead of 10.. - /// - public static string Menu_FastFlags_Presets_AltGraphicsSelector_Description { - get { - return ResourceManager.GetString("Menu.FastFlags.Presets.AltGraphicsSelector.Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use advanced graphics quality selector. - /// - public static string Menu_FastFlags_Presets_AltGraphicsSelector_Title { - get { - return ResourceManager.GetString("Menu.FastFlags.Presets.AltGraphicsSelector.Title", resourceCulture); - } - } - /// /// Looks up a localized string similar to Rendering and Graphics. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 890ec70..1285585 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -637,12 +637,6 @@ Do NOT use this to import large "flag lists" made by other people that promise t Learn more about Fast Flags, what these presets do, and how to use them. Title is Common.Help - - Allows you to configure 21 different quality levels instead of 10. - - - Use advanced graphics quality selector - Direct3D [exclusive fullscreen]({0}) using Alt+Enter is enabled by default. diff --git a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml index f8bcc5c..9af9ff0 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml @@ -165,12 +165,6 @@ - - - - diff --git a/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs index 9d0a35e..1fc04e0 100644 --- a/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs @@ -96,12 +96,6 @@ namespace Bloxstrap.UI.ViewModels.Settings set => App.FastFlags.SetPreset("Rendering.DisableScaling", value ? "True" : null); } - public bool AlternateGraphicsSelectorEnabled - { - get => App.FastFlags.GetPreset("UI.Menu.GraphicsSlider") == "True"; - set => App.FastFlags.SetPreset("UI.Menu.GraphicsSlider", value ? "True" : null); - } - public IReadOnlyDictionary> IGMenuVersions => FastFlagManager.IGMenuVersions; public InGameMenuVersion SelectedIGMenuVersion From ff387cfc59d6ef0872e92cbaefb97bc1c2aee768 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 9 Sep 2024 12:12:51 +0100 Subject: [PATCH 12/30] Fix a few remaining things --- Bloxstrap/Installer.cs | 12 ++++++++---- Bloxstrap/LaunchHandler.cs | 8 +++++++- .../UI/Elements/Dialogs/ConnectivityDialog.xaml | 2 +- Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index 2aff052..e27fe5b 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -162,11 +162,12 @@ namespace Bloxstrap const string LOG_IDENT = "Installer::DoUninstall"; var processes = new List(); - processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName)); + + if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid)) + processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName)); -#if STUDIO_FEATURES - processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName)); -#endif + if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid)) + processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName)); // prompt to shutdown roblox if its currently running if (processes.Any()) @@ -179,7 +180,10 @@ namespace Bloxstrap ); if (result != MessageBoxResult.OK) + { App.Terminate(ErrorCode.ERROR_CANCELLED); + return; + } try { diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 502936a..28b4e2b 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -1,6 +1,5 @@ using System.Windows; -using Microsoft.Win32; using Windows.Win32; using Windows.Win32.Foundation; @@ -43,6 +42,8 @@ namespace Bloxstrap LaunchRoblox(); else if (!App.LaunchSettings.QuietFlag.Active) LaunchMenu(); + else + App.Terminate(); } public static void LaunchInstaller() @@ -52,6 +53,7 @@ namespace Bloxstrap if (!interlock.IsAcquired) { Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Installer, MessageBoxImage.Stop); + App.Terminate(); return; } @@ -96,6 +98,7 @@ namespace Bloxstrap if (!interlock.IsAcquired) { Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Uninstaller, MessageBoxImage.Stop); + App.Terminate(); return; } @@ -116,7 +119,10 @@ namespace Bloxstrap } if (!confirmed) + { + App.Terminate(); return; + } Installer.DoUninstall(keepData); diff --git a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml index ca2f96b..03817bb 100644 --- a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml +++ b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml @@ -32,7 +32,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml index 1fa1c01..59ee1bc 100644 --- a/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml +++ b/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml @@ -33,7 +33,7 @@ - + From 3f0ab2239345c643016b7eca51a886d1672ea48c Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 9 Sep 2024 16:17:54 +0100 Subject: [PATCH 13/30] Address "Access to the path is denied" error --- Bloxstrap/App.xaml.cs | 11 ++------- Bloxstrap/Installer.cs | 23 +++++++++++++++---- Bloxstrap/Paths.cs | 2 -- Bloxstrap/Resources/Strings.Designer.cs | 11 +++++++++ Bloxstrap/Resources/Strings.resx | 5 ++++ .../ViewModels/Installer/InstallViewModel.cs | 6 ----- Bloxstrap/Utilities.cs | 5 ++-- 7 files changed, 39 insertions(+), 24 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index e8a91e2..373c7ee 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -57,21 +57,13 @@ namespace Bloxstrap private static bool _showingExceptionDialog = false; - private static bool _terminating = false; - public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) { - if (_terminating) - return; - int exitCodeNum = (int)exitCode; Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); - Current.Dispatcher.Invoke(() => Current.Shutdown(exitCodeNum)); - // Environment.Exit(exitCodeNum); - - _terminating = true; + Environment.Exit(exitCodeNum); } void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e) @@ -109,6 +101,7 @@ namespace Bloxstrap public static async Task GetLatestRelease() { const string LOG_IDENT = "App::GetLatestRelease"; + try { var releaseInfo = await Http.GetJson($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index e27fe5b..0fafded 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -35,7 +35,19 @@ namespace Bloxstrap if (!IsImplicitInstall) { Filesystem.AssertReadOnly(Paths.Application); - File.Copy(Paths.Process, Paths.Application, true); + + try + { + File.Copy(Paths.Process, Paths.Application, true); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, "Could not overwrite executable"); + App.Logger.WriteException(LOG_IDENT, ex); + + Frontend.ShowMessageBox(Strings.Installer_Install_CannotOverwrite, MessageBoxImage.Error); + App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); + } } // TODO: registry access checks, i'll need to look back on issues to see what the error looks like @@ -259,8 +271,10 @@ namespace Bloxstrap () => File.Delete(StartMenuShortcut), - () => Directory.Delete(Paths.Versions, true), () => Directory.Delete(Paths.Downloads, true), + () => Directory.Delete(Paths.Roblox, true), + + () => File.Delete(App.State.FileLocation) }; if (!keepData) @@ -270,8 +284,7 @@ namespace Bloxstrap () => Directory.Delete(Paths.Modifications, true), () => Directory.Delete(Paths.Logs, true), - () => File.Delete(App.Settings.FileLocation), - () => File.Delete(App.State.FileLocation), // TODO: maybe this should always be deleted? not sure yet + () => File.Delete(App.Settings.FileLocation) }); } @@ -525,6 +538,8 @@ namespace Bloxstrap } App.FastFlags.SetValue("FFlagFixGraphicsQuality", null); + + Directory.Delete(Path.Combine(Paths.Base, "Versions")); } App.Settings.Save(); diff --git a/Bloxstrap/Paths.cs b/Bloxstrap/Paths.cs index 7240c6f..36f8136 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -20,7 +20,6 @@ public static string Downloads { get; private set; } = ""; public static string Logs { get; private set; } = ""; public static string Integrations { get; private set; } = ""; - public static string Versions { get; private set; } = ""; public static string Modifications { get; private set; } = ""; public static string Roblox { get; private set; } = ""; @@ -36,7 +35,6 @@ Downloads = Path.Combine(Base, "Downloads"); Logs = Path.Combine(Base, "Logs"); Integrations = Path.Combine(Base, "Integrations"); - Versions = Path.Combine(Base, "Versions"); Modifications = Path.Combine(Base, "Modifications"); Roblox = Path.Combine(Base, "Roblox"); diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 67ab3a1..99eb9bd 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -1446,6 +1446,17 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Bloxstrap has been installed to this location before and is still present, however the installer cannot overwrite the old executable. + /// + ///Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards.. + /// + public static string Installer_Install_CannotOverwrite { + get { + return ResourceManager.GetString("Installer.Install.CannotOverwrite", resourceCulture); + } + } + /// /// Looks up a localized string similar to Existing data found. Your mods and settings will be restored.. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 1285585..0e2ebd1 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1177,4 +1177,9 @@ Are you sure you want to continue? Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue. + + Bloxstrap has been installed to this location before and is still present, however the installer cannot overwrite the old executable. + +Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards. + \ No newline at end of file diff --git a/Bloxstrap/UI/ViewModels/Installer/InstallViewModel.cs b/Bloxstrap/UI/ViewModels/Installer/InstallViewModel.cs index 6098861..8bff001 100644 --- a/Bloxstrap/UI/ViewModels/Installer/InstallViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Installer/InstallViewModel.cs @@ -2,12 +2,6 @@ using System.Windows.Input; using CommunityToolkit.Mvvm.Input; -using Bloxstrap.Resources; - -using Microsoft.Win32; -using Wpf.Ui.Mvvm.Interfaces; -using System.ComponentModel; - namespace Bloxstrap.UI.ViewModels.Installer { public class InstallViewModel : NotifyPropertyChangedViewModel diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 2b28ae7..327f734 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Security.Principal; namespace Bloxstrap { @@ -52,9 +51,9 @@ namespace Bloxstrap public static string GetRobloxVersion(bool studio) { string versionGuid = studio ? App.State.Prop.Studio.VersionGuid : App.State.Prop.Player.VersionGuid; - string fileName = studio ? "RobloxStudioBeta.exe" : "RobloxPlayerBeta.exe"; + string fileName = studio ? "Studio/RobloxStudioBeta.exe" : "Player/RobloxPlayerBeta.exe"; - string playerLocation = Path.Combine(Paths.Versions, versionGuid, fileName); + string playerLocation = Path.Combine(Paths.Roblox, fileName); if (!File.Exists(playerLocation)) return ""; From 717b57ea2a1b9dd8407ec4281025342897da902b Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 12 Sep 2024 09:52:36 +0100 Subject: [PATCH 14/30] Improve server location querying and general connectivity handling --- Bloxstrap/Bootstrapper.cs | 6 ---- Bloxstrap/GlobalCache.cs | 4 +-- Bloxstrap/Models/ActivityData.cs | 34 +++++++++++-------- .../Models/SettingTasks/EmojiModPresetTask.cs | 9 +++-- Bloxstrap/Resources/Strings.Designer.cs | 15 +++++--- Bloxstrap/Resources/Strings.resx | 9 ++--- Bloxstrap/UI/NotifyIconWrapper.cs | 2 +- .../ContextMenu/ServerInformationViewModel.cs | 8 ++++- 8 files changed, 50 insertions(+), 37 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index ee89778..62116b9 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -118,10 +118,6 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper"); - // connectivity check - - App.Logger.WriteLine(LOG_IDENT, "Performing connectivity check..."); - SetStatus(Strings.Bootstrapper_Status_Connecting); var connectionResult = await RobloxDeployment.InitializeConnectivity(); @@ -129,9 +125,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); if (connectionResult is not null) - { HandleConnectionError(connectionResult); - } #if !DEBUG || DEBUG_UPDATER if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) diff --git a/Bloxstrap/GlobalCache.cs b/Bloxstrap/GlobalCache.cs index fcd6ede..6977224 100644 --- a/Bloxstrap/GlobalCache.cs +++ b/Bloxstrap/GlobalCache.cs @@ -2,8 +2,6 @@ { public static class GlobalCache { - public static readonly Dictionary PendingTasks = new(); - - public static readonly Dictionary ServerLocation = new(); + public static readonly Dictionary ServerLocation = new(); } } diff --git a/Bloxstrap/Models/ActivityData.cs b/Bloxstrap/Models/ActivityData.cs index 3a940e6..b99e81c 100644 --- a/Bloxstrap/Models/ActivityData.cs +++ b/Bloxstrap/Models/ActivityData.cs @@ -71,6 +71,8 @@ namespace Bloxstrap.Models public ICommand RejoinServerCommand => new RelayCommand(RejoinServer); + private SemaphoreSlim serverQuerySemaphore = new(1, 1); + public string GetInviteDeeplink(bool launchData = true) { string deeplink = $"roblox://experiences/start?placeId={PlaceId}"; @@ -86,29 +88,24 @@ namespace Bloxstrap.Models return deeplink; } - public async Task QueryServerLocation() + public async Task QueryServerLocation() { const string LOG_IDENT = "ActivityData::QueryServerLocation"; if (!MachineAddressValid) throw new InvalidOperationException($"Machine address is invalid ({MachineAddress})"); - if (GlobalCache.PendingTasks.TryGetValue(MachineAddress, out Task? task)) - await task; + await serverQuerySemaphore.WaitAsync(); if (GlobalCache.ServerLocation.TryGetValue(MachineAddress, out string? location)) + { + serverQuerySemaphore.Release(); return location; + } try { - location = ""; - var ipInfoTask = Http.GetJson($"https://ipinfo.io/{MachineAddress}/json"); - - GlobalCache.PendingTasks.Add(MachineAddress, ipInfoTask); - - var ipInfo = await ipInfoTask; - - GlobalCache.PendingTasks.Remove(MachineAddress); + var ipInfo = await Http.GetJson($"https://ipinfo.io/{MachineAddress}/json"); if (String.IsNullOrEmpty(ipInfo.City)) throw new InvalidHTTPResponseException("Reported city was blank"); @@ -119,18 +116,25 @@ namespace Bloxstrap.Models location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}"; GlobalCache.ServerLocation[MachineAddress] = location; - - return location; + serverQuerySemaphore.Release(); } catch (Exception ex) { App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {MachineAddress}"); App.Logger.WriteException(LOG_IDENT, ex); - Frontend.ShowMessageBox($"{Strings.ActivityWatcher_LocationQueryFailed}\n\n{ex.Message}", MessageBoxImage.Warning); + GlobalCache.ServerLocation[MachineAddress] = location; + serverQuerySemaphore.Release(); - return "?"; + Frontend.ShowConnectivityDialog( + String.Format(Strings.Dialog_Connectivity_UnableToConnect, "ipinfo.io"), + Strings.ActivityWatcher_LocationQueryFailed, + MessageBoxImage.Warning, + ex + ); } + + return location; } public override string ToString() => $"{PlaceId}/{JobId}"; diff --git a/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs b/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs index 08c0edf..89c8d92 100644 --- a/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs +++ b/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs @@ -52,9 +52,12 @@ namespace Bloxstrap.Models.SettingTasks { App.Logger.WriteException(LOG_IDENT, ex); - Frontend.ShowMessageBox( - String.Format(Strings.Menu_Mods_Presets_EmojiType_Error, ex.Message), - MessageBoxImage.Warning); + Frontend.ShowConnectivityDialog( + String.Format(Strings.Dialog_Connectivity_UnableToConnect, "GitHub"), + $"{Strings.Menu_Mods_Presets_EmojiType_Error}\n\n{Strings.Dialog_Connectivity_TryAgainLater}", + MessageBoxImage.Warning, + ex + ); } } else if (query is not null && query.Any()) diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 99eb9bd..1462950 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -106,7 +106,7 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Failed to query server location.. + /// Looks up a localized string similar to The server location could not be queried. You may be joining games too quickly.. /// public static string ActivityWatcher_LocationQueryFailed { get { @@ -512,6 +512,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Not available. + /// + public static string Common_NotAvailable { + get { + return ResourceManager.GetString("Common.NotAvailable", resourceCulture); + } + } + /// /// Looks up a localized string similar to OK. /// @@ -2891,9 +2900,7 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to The emoji mod could not be applied because of a network error during download. - /// - ///{0}. + /// Looks up a localized string similar to The emoji mod can not be applied at this time.. /// public static string Menu_Mods_Presets_EmojiType_Error { get { diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 0e2ebd1..8cedffc 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1096,9 +1096,7 @@ If not, then please report this exception to the maintainers of this fork. Do NO Connected to reserved server - The emoji mod could not be applied because of a network error during download. - -{0} + The emoji mod can not be applied at this time. Please wait for installation to finish. @@ -1163,7 +1161,7 @@ Are you sure you want to continue? Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them. - Failed to query server location. + The server location could not be queried. You may be joining games too quickly. {0} may be down right now. @@ -1182,4 +1180,7 @@ Are you sure you want to continue? Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards. + + Not available + \ No newline at end of file diff --git a/Bloxstrap/UI/NotifyIconWrapper.cs b/Bloxstrap/UI/NotifyIconWrapper.cs index d15b2d0..85dfac7 100644 --- a/Bloxstrap/UI/NotifyIconWrapper.cs +++ b/Bloxstrap/UI/NotifyIconWrapper.cs @@ -59,7 +59,7 @@ namespace Bloxstrap.UI if (_activityWatcher is null) return; - string serverLocation = await _activityWatcher.Data.QueryServerLocation(); + string? serverLocation = await _activityWatcher.Data.QueryServerLocation(); if (string.IsNullOrEmpty(serverLocation)) return; diff --git a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs index 420b916..8e1eccd 100644 --- a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs +++ b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs @@ -29,7 +29,13 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu public async void QueryServerLocation() { - ServerLocation = await _activityWatcher.Data.QueryServerLocation(); + string? location = await _activityWatcher.Data.QueryServerLocation(); + + if (String.IsNullOrEmpty(location)) + ServerLocation = Strings.Common_NotAvailable; + else + ServerLocation = location; + OnPropertyChanged(nameof(ServerLocation)); } From c5fa2c0bc0ee174c76d81e429443143c27e92bda Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 12 Sep 2024 10:00:19 +0100 Subject: [PATCH 15/30] Hide game history if desktop app is disabled --- Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml | 4 ++-- Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml index a3a4754..b3e08e2 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml @@ -49,7 +49,7 @@ - + @@ -61,7 +61,7 @@ - + diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs index 388752e..d55c3dd 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs @@ -2,16 +2,11 @@ using System.Windows.Controls; using System.Windows.Interop; -using Wpf.Ui.Appearance; -using Wpf.Ui.Mvvm.Contracts; -using Wpf.Ui.Mvvm.Services; - using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; using Bloxstrap.Integrations; -using Bloxstrap.Resources; namespace Bloxstrap.UI.Elements.ContextMenu { @@ -46,6 +41,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu if (_watcher.RichPresence is not null) RichPresenceMenuItem.Visibility = Visibility.Visible; + if (!App.Settings.Prop.UseDisableAppPatch) + GameHistoryMenuItem.Visibility = Visibility.Visible; + VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}"; } From 1b60e30a0bb47bba937b20d51ee629a750665e25 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 12 Sep 2024 10:53:43 +0100 Subject: [PATCH 16/30] Add easy access to player compatibility settings --- Bloxstrap/NativeMethods.txt | 2 +- Bloxstrap/Resources/Strings.Designer.cs | 27 +++++++++++++++++++ Bloxstrap/Resources/Strings.resx | 9 +++++++ .../UI/Elements/Settings/Pages/ModsPage.xaml | 9 +++++++ .../Settings/IntegrationsViewModel.cs | 4 +-- .../UI/ViewModels/Settings/ModsViewModel.cs | 18 +++++++++++++ 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/NativeMethods.txt b/Bloxstrap/NativeMethods.txt index aaa0b6a..9c1249b 100644 --- a/Bloxstrap/NativeMethods.txt +++ b/Bloxstrap/NativeMethods.txt @@ -2,4 +2,4 @@ FlashWindow GetWindowLong SetWindowLong -EnumDisplaySettings +SHObjectProperties \ No newline at end of file diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 1462950..7911c5b 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -557,6 +557,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option.. + /// + public static string Common_RobloxNotInstalled { + get { + return ResourceManager.GetString("Common.RobloxNotInstalled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shortcuts. /// @@ -2809,6 +2818,24 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Configure application parameters such as DPI scaling behaviour and [fullscreen optimizations]({0}).. + /// + public static string Menu_Mods_Misc_CompatibilitySettings_Description { + get { + return ResourceManager.GetString("Menu.Mods.Misc.CompatibilitySettings.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manage compatibility settings. + /// + public static string Menu_Mods_Misc_CompatibilitySettings_Title { + get { + return ResourceManager.GetString("Menu.Mods.Misc.CompatibilitySettings.Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Choose font.... /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 8cedffc..0a04ef2 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1183,4 +1183,13 @@ Please manually delete Bloxstrap.exe from the install location or try restarting Not available + + Manage compatibility settings + + + Configure application parameters such as DPI scaling behaviour and [fullscreen optimizations]({0}). + + + Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option. + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml index af34a12..79e6627 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml @@ -25,18 +25,27 @@ + + + + + + + + + diff --git a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs index 3ea1352..aed332e 100644 --- a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs @@ -1,8 +1,6 @@ using System.Collections.ObjectModel; using System.Windows.Input; -using Bloxstrap.Resources; - using Microsoft.Win32; using CommunityToolkit.Mvvm.Input; @@ -12,7 +10,9 @@ namespace Bloxstrap.UI.ViewModels.Settings public class IntegrationsViewModel : NotifyPropertyChangedViewModel { public ICommand AddIntegrationCommand => new RelayCommand(AddIntegration); + public ICommand DeleteIntegrationCommand => new RelayCommand(DeleteIntegration); + public ICommand BrowseIntegrationLocationCommand => new RelayCommand(BrowseIntegrationLocation); private void AddIntegration() diff --git a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs index 0e85f6c..f235af0 100644 --- a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs @@ -3,9 +3,14 @@ using System.Windows.Input; using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.UI.Shell; +using Windows.Win32.Foundation; + using CommunityToolkit.Mvvm.Input; using Bloxstrap.Models.SettingTasks; +using Bloxstrap.AppData; namespace Bloxstrap.UI.ViewModels.Settings { @@ -59,6 +64,8 @@ namespace Bloxstrap.UI.ViewModels.Settings public ICommand ManageCustomFontCommand => new RelayCommand(ManageCustomFont); + public ICommand OpenCompatSettingsCommand => new RelayCommand(OpenCompatSettings); + public ModPresetTask OldDeathSoundTask { get; } = new("OldDeathSound", @"content\sounds\ouch.ogg", "Sounds.OldDeath.ogg"); public ModPresetTask OldAvatarBackgroundTask { get; } = new("OldAvatarBackground", @"ExtraContent\places\Mobile.rbxl", "OldAvatarBackground.rbxl"); @@ -95,5 +102,16 @@ namespace Bloxstrap.UI.ViewModels.Settings }); public FontModPresetTask TextFontTask { get; } = new(); + + private void OpenCompatSettings() + { + string path = new RobloxPlayerData().ExecutablePath; + + if (File.Exists(path)) + PInvoke.SHObjectProperties(HWND.Null, SHOP_TYPE.SHOP_FILEPATH, path, "Compatibility"); + else + Frontend.ShowMessageBox(Strings.Common_RobloxNotInstalled, MessageBoxImage.Error); + + } } } From 47cf9e70cbf558a7d307cacd5fda80b046dd2a80 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 12 Sep 2024 22:05:27 +0100 Subject: [PATCH 17/30] Auto updater shim for <2.8.0 versions --- Bloxstrap/Installer.cs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index 0fafded..11e7b35 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -394,15 +394,30 @@ namespace Bloxstrap } } - try + // prior to 2.8.0, auto-updating was handled with this... bruteforce method + // now it's handled with the system mutex you see above, but we need to keep this logic for <2.8.0 versions + for (int i = 1; i <= 10; i++) { - File.Copy(Paths.Process, Paths.Application, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not replace executable)"); - App.Logger.WriteException(LOG_IDENT, ex); - return; + try + { + File.Copy(Paths.Process, Paths.Application, true); + break; + } + catch (Exception ex) + { + if (i == 1) + { + App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); + } + else if (i == 10) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 10 tries/5 seconds)"); + App.Logger.WriteException(LOG_IDENT, ex); + return; + } + + Thread.Sleep(500); + } } using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) From 83e3c487a9fde33a59103b32a5ae84c8a4679938 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 13 Sep 2024 00:28:57 +0100 Subject: [PATCH 18/30] Persistence of settings window size/position (#2319) should be good enough imo --- Bloxstrap/LaunchHandler.cs | 4 ++- Bloxstrap/Models/State.cs | 2 ++ Bloxstrap/Models/WindowState.cs | 13 +++++++ .../UI/Elements/Settings/MainWindow.xaml.cs | 34 +++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Bloxstrap/Models/WindowState.cs diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 28b4e2b..891a7ed 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -140,7 +140,9 @@ namespace Bloxstrap if (interlock.IsAcquired) { bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1; - new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).Show(); + + var window = new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning); + window.Show(); } else { diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index 45e2ef4..edaf426 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -10,6 +10,8 @@ public AppState Studio { get; set; } = new(); + public WindowState SettingsWindow { get; set; } = new(); + public List ModManifest { get; set; } = new(); } } diff --git a/Bloxstrap/Models/WindowState.cs b/Bloxstrap/Models/WindowState.cs new file mode 100644 index 0000000..efc808d --- /dev/null +++ b/Bloxstrap/Models/WindowState.cs @@ -0,0 +1,13 @@ +namespace Bloxstrap.Models +{ + public class WindowState + { + public double Width { get; set; } + + public double Height { get; set; } + + public double Left { get; set; } + + public double Top { get; set; } + } +} diff --git a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs index 59bd1db..df17658 100644 --- a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs +++ b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs @@ -14,6 +14,8 @@ namespace Bloxstrap.UI.Elements.Settings /// public partial class MainWindow : INavigationWindow { + private Models.WindowState _state => App.State.Prop.SettingsWindow; + public MainWindow(bool showAlreadyRunningWarning) { var viewModel = new MainWindowViewModel(); @@ -33,6 +35,30 @@ namespace Bloxstrap.UI.Elements.Settings if (showAlreadyRunningWarning) ShowAlreadyRunningSnackbar(); + + LoadState(); + } + + public void LoadState() + { + if (_state.Left > SystemParameters.VirtualScreenWidth) + _state.Left = 0; + + if (_state.Top > SystemParameters.VirtualScreenHeight) + _state.Top = 0; + + if (_state.Width > 0) + this.Width = _state.Width; + + if (_state.Height > 0) + this.Height = _state.Height; + + if (_state.Left > 0 && _state.Top > 0) + { + this.WindowStartupLocation = WindowStartupLocation.Manual; + this.Left = _state.Left; + this.Top = _state.Top; + } } private async void ShowAlreadyRunningSnackbar() @@ -66,6 +92,14 @@ namespace Bloxstrap.UI.Elements.Settings if (result != MessageBoxResult.Yes) e.Cancel = true; } + + _state.Width = this.Width; + _state.Height = this.Height; + + _state.Top = this.Top; + _state.Left = this.Left; + + App.State.Save(); if (!e.Cancel) App.Terminate(); From 263617e40918b52165b5a9615f7143d14af5cbce Mon Sep 17 00:00:00 2001 From: Flikter <83476766+Flikter@users.noreply.github.com> Date: Sun, 15 Sep 2024 01:25:14 +0200 Subject: [PATCH 19/30] Autofill file name --- Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs index aed332e..042b138 100644 --- a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs @@ -57,6 +57,7 @@ namespace Bloxstrap.UI.ViewModels.Settings if (dialog.ShowDialog() != true) return; + SelectedCustomIntegration.Name = dialog.SafeFileName; SelectedCustomIntegration.Location = dialog.FileName; OnPropertyChanged(nameof(SelectedCustomIntegration)); } From 18093590257d3f4e09002ac4bc6b6354cba43b22 Mon Sep 17 00:00:00 2001 From: Flikter <83476766+Flikter@users.noreply.github.com> Date: Sun, 15 Sep 2024 01:25:42 +0200 Subject: [PATCH 20/30] Update integration name while typing --- Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml index 8bafca9..9f409a9 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml @@ -89,7 +89,7 @@ - + From 6e8faff62406b856dc3018d37db2e260c238c7fb Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 15 Sep 2024 18:26:42 +0100 Subject: [PATCH 21/30] Improve querying of app theme preference --- Bloxstrap/Extensions/ThemeEx.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Bloxstrap/Extensions/ThemeEx.cs b/Bloxstrap/Extensions/ThemeEx.cs index 30da539..f5fc70c 100644 --- a/Bloxstrap/Extensions/ThemeEx.cs +++ b/Bloxstrap/Extensions/ThemeEx.cs @@ -9,15 +9,10 @@ namespace Bloxstrap.Extensions if (dialogTheme != Theme.Default) return dialogTheme; - RegistryKey? key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); + using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); - if (key is not null) - { - var value = key.GetValue("AppsUseLightTheme"); - - if (value is not null && (int)value == 0) - return Theme.Dark; - } + if (key?.GetValue("AppsUseLightTheme") is int value && value == 0) + return Theme.Dark; return Theme.Light; } From c58a8ab7397a5589e29291f0fde5c9bf4b8773ea Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 15 Sep 2024 18:58:00 +0100 Subject: [PATCH 22/30] Ensure installation ignores temp path (#2892) --- Bloxstrap/Installer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index 11e7b35..9e2f6f2 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -104,6 +104,10 @@ namespace Bloxstrap if (InstallLocation.StartsWith("\\\\")) return false; + if (InstallLocation.StartsWith(Path.GetTempPath(), StringComparison.InvariantCultureIgnoreCase) + || InstallLocation.Contains("\\Temp\\", StringComparison.InvariantCultureIgnoreCase)) + return false; + // prevent from installing to a onedrive folder if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase)) return false; From 0b20720fdb3cb4ee4870b448d9edc4d3a8e5bfb1 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 15 Sep 2024 20:58:28 +0100 Subject: [PATCH 23/30] Reorganize models --- Bloxstrap/GlobalUsings.cs | 9 +++-- .../Models/{ => APIs/Config}/Supporter.cs | 2 +- .../Models/{ => APIs/Config}/SupporterData.cs | 2 +- .../Models/APIs/GitHub/GitHubReleaseAsset.cs | 8 +++++ .../Models/{ => APIs/GitHub}/GithubRelease.cs | 13 ++----- Bloxstrap/Models/{ => APIs}/IPInfoResponse.cs | 28 +++++++-------- .../Roblox}/ApiArrayResponse.cs | 2 +- .../{ => APIs/Roblox}/ClientFlagSettings.cs | 2 +- .../Models/{ => APIs/Roblox}/ClientVersion.cs | 2 +- .../{RobloxApi => APIs/Roblox}/GameCreator.cs | 2 +- .../Roblox}/GameDetailResponse.cs | 2 +- .../Roblox}/ThumbnailResponse.cs | 2 +- .../Roblox}/UniverseIdResponse.cs | 2 +- .../Models/{ => Entities}/ActivityData.cs | 36 +++++++++---------- .../{ => Entities}/ModPresetFileData.cs | 8 ++--- .../Models/{ => Entities}/UniverseDetails.cs | 10 +++--- .../Models/{ => Persistable}/AppState.cs | 4 +-- .../Models/{ => Persistable}/Settings.cs | 2 +- Bloxstrap/Models/{ => Persistable}/State.cs | 6 ++-- .../Models/{ => Persistable}/WindowState.cs | 8 ++--- .../Models/SettingTasks/EnumModPresetTask.cs | 3 +- .../Models/SettingTasks/ModPresetTask.cs | 3 +- .../UI/Elements/Settings/MainWindow.xaml.cs | 2 +- 23 files changed, 83 insertions(+), 75 deletions(-) rename Bloxstrap/Models/{ => APIs/Config}/Supporter.cs (89%) rename Bloxstrap/Models/{ => APIs/Config}/SupporterData.cs (84%) create mode 100644 Bloxstrap/Models/APIs/GitHub/GitHubReleaseAsset.cs rename Bloxstrap/Models/{ => APIs/GitHub}/GithubRelease.cs (63%) rename Bloxstrap/Models/{ => APIs}/IPInfoResponse.cs (88%) rename Bloxstrap/Models/{RobloxApi => APIs/Roblox}/ApiArrayResponse.cs (84%) rename Bloxstrap/Models/{ => APIs/Roblox}/ClientFlagSettings.cs (80%) rename Bloxstrap/Models/{ => APIs/Roblox}/ClientVersion.cs (91%) rename Bloxstrap/Models/{RobloxApi => APIs/Roblox}/GameCreator.cs (96%) rename Bloxstrap/Models/{RobloxApi => APIs/Roblox}/GameDetailResponse.cs (99%) rename Bloxstrap/Models/{RobloxApi => APIs/Roblox}/ThumbnailResponse.cs (90%) rename Bloxstrap/Models/{RobloxApi => APIs/Roblox}/UniverseIdResponse.cs (80%) rename Bloxstrap/Models/{ => Entities}/ActivityData.cs (85%) rename Bloxstrap/Models/{ => Entities}/ModPresetFileData.cs (95%) rename Bloxstrap/Models/{ => Entities}/UniverseDetails.cs (90%) rename Bloxstrap/Models/{ => Persistable}/AppState.cs (60%) rename Bloxstrap/Models/{ => Persistable}/Settings.cs (97%) rename Bloxstrap/Models/{ => Persistable}/State.cs (89%) rename Bloxstrap/Models/{ => Persistable}/WindowState.cs (78%) diff --git a/Bloxstrap/GlobalUsings.cs b/Bloxstrap/GlobalUsings.cs index 9c10516..c04aec2 100644 --- a/Bloxstrap/GlobalUsings.cs +++ b/Bloxstrap/GlobalUsings.cs @@ -3,7 +3,6 @@ global using System.Collections.Generic; global using System.Diagnostics; global using System.Globalization; global using System.IO; -global using System.IO.Compression; global using System.Text; global using System.Text.Json; global using System.Text.Json.Serialization; @@ -18,10 +17,16 @@ global using Bloxstrap.Enums; global using Bloxstrap.Exceptions; global using Bloxstrap.Extensions; global using Bloxstrap.Models; +global using Bloxstrap.Models.APIs.Config; +global using Bloxstrap.Models.APIs.GitHub; +global using Bloxstrap.Models.APIs.Roblox; global using Bloxstrap.Models.Attributes; global using Bloxstrap.Models.BloxstrapRPC; -global using Bloxstrap.Models.RobloxApi; +global using Bloxstrap.Models.Entities; global using Bloxstrap.Models.Manifest; +global using Bloxstrap.Models.Persistable; +global using Bloxstrap.Models.SettingTasks; +global using Bloxstrap.Models.SettingTasks.Base; global using Bloxstrap.Resources; global using Bloxstrap.UI; global using Bloxstrap.Utility; \ No newline at end of file diff --git a/Bloxstrap/Models/Supporter.cs b/Bloxstrap/Models/APIs/Config/Supporter.cs similarity index 89% rename from Bloxstrap/Models/Supporter.cs rename to Bloxstrap/Models/APIs/Config/Supporter.cs index 6732a26..9a689b5 100644 --- a/Bloxstrap/Models/Supporter.cs +++ b/Bloxstrap/Models/APIs/Config/Supporter.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.APIs.Config { public class Supporter { diff --git a/Bloxstrap/Models/SupporterData.cs b/Bloxstrap/Models/APIs/Config/SupporterData.cs similarity index 84% rename from Bloxstrap/Models/SupporterData.cs rename to Bloxstrap/Models/APIs/Config/SupporterData.cs index f9ef2fe..35feb1e 100644 --- a/Bloxstrap/Models/SupporterData.cs +++ b/Bloxstrap/Models/APIs/Config/SupporterData.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.APIs.Config { public class SupporterData { diff --git a/Bloxstrap/Models/APIs/GitHub/GitHubReleaseAsset.cs b/Bloxstrap/Models/APIs/GitHub/GitHubReleaseAsset.cs new file mode 100644 index 0000000..023c307 --- /dev/null +++ b/Bloxstrap/Models/APIs/GitHub/GitHubReleaseAsset.cs @@ -0,0 +1,8 @@ +public class GithubReleaseAsset +{ + [JsonPropertyName("browser_download_url")] + public string BrowserDownloadUrl { get; set; } = null!; + + [JsonPropertyName("name")] + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/Bloxstrap/Models/GithubRelease.cs b/Bloxstrap/Models/APIs/GitHub/GithubRelease.cs similarity index 63% rename from Bloxstrap/Models/GithubRelease.cs rename to Bloxstrap/Models/APIs/GitHub/GithubRelease.cs index 12b8876..2f55f91 100644 --- a/Bloxstrap/Models/GithubRelease.cs +++ b/Bloxstrap/Models/APIs/GitHub/GithubRelease.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.APIs.GitHub { public class GithubRelease { @@ -7,7 +7,7 @@ [JsonPropertyName("name")] public string Name { get; set; } = null!; - + [JsonPropertyName("body")] public string Body { get; set; } = null!; @@ -17,13 +17,4 @@ [JsonPropertyName("assets")] public List? Assets { get; set; } } - - public class GithubReleaseAsset - { - [JsonPropertyName("browser_download_url")] - public string BrowserDownloadUrl { get; set; } = null!; - - [JsonPropertyName("name")] - public string Name { get; set; } = null!; - } } diff --git a/Bloxstrap/Models/IPInfoResponse.cs b/Bloxstrap/Models/APIs/IPInfoResponse.cs similarity index 88% rename from Bloxstrap/Models/IPInfoResponse.cs rename to Bloxstrap/Models/APIs/IPInfoResponse.cs index f8d3bc0..8cb65a5 100644 --- a/Bloxstrap/Models/IPInfoResponse.cs +++ b/Bloxstrap/Models/APIs/IPInfoResponse.cs @@ -1,14 +1,14 @@ -namespace Bloxstrap.Models -{ - public class IPInfoResponse - { - [JsonPropertyName("city")] - public string City { get; set; } = null!; - - [JsonPropertyName("country")] - public string Country { get; set; } = null!; - - [JsonPropertyName("region")] - public string Region { get; set; } = null!; - } -} +namespace Bloxstrap.Models.APIs +{ + public class IPInfoResponse + { + [JsonPropertyName("city")] + public string City { get; set; } = null!; + + [JsonPropertyName("country")] + public string Country { get; set; } = null!; + + [JsonPropertyName("region")] + public string Region { get; set; } = null!; + } +} diff --git a/Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs b/Bloxstrap/Models/APIs/Roblox/ApiArrayResponse.cs similarity index 84% rename from Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs rename to Bloxstrap/Models/APIs/Roblox/ApiArrayResponse.cs index 4e1fefd..202de43 100644 --- a/Bloxstrap/Models/RobloxApi/ApiArrayResponse.cs +++ b/Bloxstrap/Models/APIs/Roblox/ApiArrayResponse.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.APIs.Roblox { /// /// Roblox.Web.WebAPI.Models.ApiArrayResponse diff --git a/Bloxstrap/Models/ClientFlagSettings.cs b/Bloxstrap/Models/APIs/Roblox/ClientFlagSettings.cs similarity index 80% rename from Bloxstrap/Models/ClientFlagSettings.cs rename to Bloxstrap/Models/APIs/Roblox/ClientFlagSettings.cs index b50932b..88ef9eb 100644 --- a/Bloxstrap/Models/ClientFlagSettings.cs +++ b/Bloxstrap/Models/APIs/Roblox/ClientFlagSettings.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.APIs.Roblox { public class ClientFlagSettings { diff --git a/Bloxstrap/Models/ClientVersion.cs b/Bloxstrap/Models/APIs/Roblox/ClientVersion.cs similarity index 91% rename from Bloxstrap/Models/ClientVersion.cs rename to Bloxstrap/Models/APIs/Roblox/ClientVersion.cs index b90df0a..9fa405e 100644 --- a/Bloxstrap/Models/ClientVersion.cs +++ b/Bloxstrap/Models/APIs/Roblox/ClientVersion.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.APIs.Roblox { public class ClientVersion { diff --git a/Bloxstrap/Models/RobloxApi/GameCreator.cs b/Bloxstrap/Models/APIs/Roblox/GameCreator.cs similarity index 96% rename from Bloxstrap/Models/RobloxApi/GameCreator.cs rename to Bloxstrap/Models/APIs/Roblox/GameCreator.cs index c191884..5ac73fd 100644 --- a/Bloxstrap/Models/RobloxApi/GameCreator.cs +++ b/Bloxstrap/Models/APIs/Roblox/GameCreator.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.APIs.Roblox { /// /// Roblox.Games.Api.Models.Response.GameCreator diff --git a/Bloxstrap/Models/RobloxApi/GameDetailResponse.cs b/Bloxstrap/Models/APIs/Roblox/GameDetailResponse.cs similarity index 99% rename from Bloxstrap/Models/RobloxApi/GameDetailResponse.cs rename to Bloxstrap/Models/APIs/Roblox/GameDetailResponse.cs index dca8c81..93abd5d 100644 --- a/Bloxstrap/Models/RobloxApi/GameDetailResponse.cs +++ b/Bloxstrap/Models/APIs/Roblox/GameDetailResponse.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.APIs.Roblox { /// diff --git a/Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs b/Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs similarity index 90% rename from Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs rename to Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs index c667813..213083c 100644 --- a/Bloxstrap/Models/RobloxApi/ThumbnailResponse.cs +++ b/Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.APIs.Roblox { /// /// Roblox.Web.Responses.Thumbnails.ThumbnailResponse diff --git a/Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs b/Bloxstrap/Models/APIs/Roblox/UniverseIdResponse.cs similarity index 80% rename from Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs rename to Bloxstrap/Models/APIs/Roblox/UniverseIdResponse.cs index c42d10a..7ab52fa 100644 --- a/Bloxstrap/Models/RobloxApi/UniverseIdResponse.cs +++ b/Bloxstrap/Models/APIs/Roblox/UniverseIdResponse.cs @@ -1,4 +1,4 @@ -namespace Bloxstrap.Models.RobloxApi +namespace Bloxstrap.Models.APIs.Roblox { // lmao its just one property public class UniverseIdResponse diff --git a/Bloxstrap/Models/ActivityData.cs b/Bloxstrap/Models/Entities/ActivityData.cs similarity index 85% rename from Bloxstrap/Models/ActivityData.cs rename to Bloxstrap/Models/Entities/ActivityData.cs index b99e81c..40565f0 100644 --- a/Bloxstrap/Models/ActivityData.cs +++ b/Bloxstrap/Models/Entities/ActivityData.cs @@ -1,10 +1,10 @@ using System.Web; using System.Windows; using System.Windows.Input; - +using Bloxstrap.Models.APIs; using CommunityToolkit.Mvvm.Input; -namespace Bloxstrap.Models +namespace Bloxstrap.Models.Entities { public class ActivityData { @@ -16,7 +16,7 @@ namespace Bloxstrap.Models /// public ActivityData? RootActivity; - public long UniverseId + public long UniverseId { get => _universeId; set @@ -28,19 +28,19 @@ namespace Bloxstrap.Models public long PlaceId { get; set; } = 0; - public string JobId { get; set; } = String.Empty; + public string JobId { get; set; } = string.Empty; /// /// This will be empty unless the server joined is a private server /// - public string AccessCode { get; set; } = String.Empty; - - public string MachineAddress { get; set; } = String.Empty; + public string AccessCode { get; set; } = string.Empty; - public bool MachineAddressValid => !String.IsNullOrEmpty(MachineAddress) && !MachineAddress.StartsWith("10."); + public string MachineAddress { get; set; } = string.Empty; + + public bool MachineAddressValid => !string.IsNullOrEmpty(MachineAddress) && !MachineAddress.StartsWith("10."); public bool IsTeleport { get; set; } = false; - + public ServerType ServerType { get; set; } = ServerType.Public; public DateTime TimeJoined { get; set; } @@ -52,15 +52,15 @@ namespace Bloxstrap.Models /// /// This is intended only for other people to use, i.e. context menu invite link, rich presence joining /// - public string RPCLaunchData { get; set; } = String.Empty; + public string RPCLaunchData { get; set; } = string.Empty; public UniverseDetails? UniverseDetails { get; set; } - + public string GameHistoryDescription { get { - string desc = String.Format("{0} • {1} - {2}", UniverseDetails?.Data.Creator.Name, TimeJoined.ToString("h:mm tt"), TimeLeft?.ToString("h:mm tt")); + string desc = string.Format("{0} • {1} - {2}", UniverseDetails?.Data.Creator.Name, TimeJoined.ToString("h:mm tt"), TimeLeft?.ToString("h:mm tt")); if (ServerType != ServerType.Public) desc += " • " + ServerType.ToTranslatedString(); @@ -82,7 +82,7 @@ namespace Bloxstrap.Models else deeplink += "&gameInstanceId=" + JobId; - if (launchData && !String.IsNullOrEmpty(RPCLaunchData)) + if (launchData && !string.IsNullOrEmpty(RPCLaunchData)) deeplink += "&launchData=" + HttpUtility.UrlEncode(RPCLaunchData); return deeplink; @@ -107,7 +107,7 @@ namespace Bloxstrap.Models { var ipInfo = await Http.GetJson($"https://ipinfo.io/{MachineAddress}/json"); - if (String.IsNullOrEmpty(ipInfo.City)) + if (string.IsNullOrEmpty(ipInfo.City)) throw new InvalidHTTPResponseException("Reported city was blank"); if (ipInfo.City == ipInfo.Region) @@ -127,9 +127,9 @@ namespace Bloxstrap.Models serverQuerySemaphore.Release(); Frontend.ShowConnectivityDialog( - String.Format(Strings.Dialog_Connectivity_UnableToConnect, "ipinfo.io"), - Strings.ActivityWatcher_LocationQueryFailed, - MessageBoxImage.Warning, + string.Format(Strings.Dialog_Connectivity_UnableToConnect, "ipinfo.io"), + Strings.ActivityWatcher_LocationQueryFailed, + MessageBoxImage.Warning, ex ); } @@ -142,7 +142,7 @@ namespace Bloxstrap.Models private void RejoinServer() { string playerPath = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe"); - + Process.Start(playerPath, GetInviteDeeplink(false)); } } diff --git a/Bloxstrap/Models/ModPresetFileData.cs b/Bloxstrap/Models/Entities/ModPresetFileData.cs similarity index 95% rename from Bloxstrap/Models/ModPresetFileData.cs rename to Bloxstrap/Models/Entities/ModPresetFileData.cs index d704891..8f620bd 100644 --- a/Bloxstrap/Models/ModPresetFileData.cs +++ b/Bloxstrap/Models/Entities/ModPresetFileData.cs @@ -1,23 +1,23 @@ using System.Security.Cryptography; using System.Windows.Markup; -namespace Bloxstrap.Models +namespace Bloxstrap.Models.Entities { public class ModPresetFileData { public string FilePath { get; private set; } public string FullFilePath => Path.Combine(Paths.Modifications, FilePath); - + public FileStream FileStream => File.OpenRead(FullFilePath); public string ResourceIdentifier { get; private set; } - + public Stream ResourceStream => Resource.GetStream(ResourceIdentifier); public byte[] ResourceHash { get; private set; } - public ModPresetFileData(string contentPath, string resource) + public ModPresetFileData(string contentPath, string resource) { FilePath = contentPath; ResourceIdentifier = resource; diff --git a/Bloxstrap/Models/UniverseDetails.cs b/Bloxstrap/Models/Entities/UniverseDetails.cs similarity index 90% rename from Bloxstrap/Models/UniverseDetails.cs rename to Bloxstrap/Models/Entities/UniverseDetails.cs index aa87501..62f5445 100644 --- a/Bloxstrap/Models/UniverseDetails.cs +++ b/Bloxstrap/Models/Entities/UniverseDetails.cs @@ -1,11 +1,13 @@ -namespace Bloxstrap.Models +using Bloxstrap.Models.APIs.Roblox; + +namespace Bloxstrap.Models.Entities { public class UniverseDetails { private static List _cache { get; set; } = new(); public GameDetailResponse Data { get; set; } = null!; - + /// /// Returns data for a 128x128 icon /// @@ -13,9 +15,9 @@ public static UniverseDetails? LoadFromCache(long id) { - var cacheQuery = _cache.Where(x => x.Data?.Id == id); + var cacheQuery = _cache.Where(x => x.Data?.Id == id); - if (cacheQuery.Any()) + if (cacheQuery.Any()) return cacheQuery.First(); return null; diff --git a/Bloxstrap/Models/AppState.cs b/Bloxstrap/Models/Persistable/AppState.cs similarity index 60% rename from Bloxstrap/Models/AppState.cs rename to Bloxstrap/Models/Persistable/AppState.cs index 68c3c58..57c3224 100644 --- a/Bloxstrap/Models/AppState.cs +++ b/Bloxstrap/Models/Persistable/AppState.cs @@ -1,8 +1,8 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.Persistable { public class AppState { - public string VersionGuid { get; set; } = String.Empty; + public string VersionGuid { get; set; } = string.Empty; public Dictionary PackageHashes { get; set; } = new(); diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs similarity index 97% rename from Bloxstrap/Models/Settings.cs rename to Bloxstrap/Models/Persistable/Settings.cs index ebe594f..d4018ce 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Persistable/Settings.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace Bloxstrap.Models +namespace Bloxstrap.Models.Persistable { public class Settings { diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/Persistable/State.cs similarity index 89% rename from Bloxstrap/Models/State.cs rename to Bloxstrap/Models/Persistable/State.cs index edaf426..de05265 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/Persistable/State.cs @@ -1,13 +1,13 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.Persistable { public class State { public bool ShowFFlagEditorWarning { get; set; } = true; - + public bool PromptWebView2Install { get; set; } = true; public AppState Player { get; set; } = new(); - + public AppState Studio { get; set; } = new(); public WindowState SettingsWindow { get; set; } = new(); diff --git a/Bloxstrap/Models/WindowState.cs b/Bloxstrap/Models/Persistable/WindowState.cs similarity index 78% rename from Bloxstrap/Models/WindowState.cs rename to Bloxstrap/Models/Persistable/WindowState.cs index efc808d..90bf334 100644 --- a/Bloxstrap/Models/WindowState.cs +++ b/Bloxstrap/Models/Persistable/WindowState.cs @@ -1,13 +1,13 @@ -namespace Bloxstrap.Models +namespace Bloxstrap.Models.Persistable { public class WindowState { public double Width { get; set; } - + public double Height { get; set; } - + public double Left { get; set; } - + public double Top { get; set; } } } diff --git a/Bloxstrap/Models/SettingTasks/EnumModPresetTask.cs b/Bloxstrap/Models/SettingTasks/EnumModPresetTask.cs index a06808e..07fb0ca 100644 --- a/Bloxstrap/Models/SettingTasks/EnumModPresetTask.cs +++ b/Bloxstrap/Models/SettingTasks/EnumModPresetTask.cs @@ -1,4 +1,5 @@ -using Bloxstrap.Models.SettingTasks.Base; +using Bloxstrap.Models.Entities; +using Bloxstrap.Models.SettingTasks.Base; namespace Bloxstrap.Models.SettingTasks { diff --git a/Bloxstrap/Models/SettingTasks/ModPresetTask.cs b/Bloxstrap/Models/SettingTasks/ModPresetTask.cs index 99c20ee..8aa1592 100644 --- a/Bloxstrap/Models/SettingTasks/ModPresetTask.cs +++ b/Bloxstrap/Models/SettingTasks/ModPresetTask.cs @@ -1,4 +1,5 @@ -using Bloxstrap.Models.SettingTasks.Base; +using Bloxstrap.Models.Entities; +using Bloxstrap.Models.SettingTasks.Base; namespace Bloxstrap.Models.SettingTasks { diff --git a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs index df17658..fda449f 100644 --- a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs +++ b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs @@ -14,7 +14,7 @@ namespace Bloxstrap.UI.Elements.Settings /// public partial class MainWindow : INavigationWindow { - private Models.WindowState _state => App.State.Prop.SettingsWindow; + private Models.Persistable.WindowState _state => App.State.Prop.SettingsWindow; public MainWindow(bool showAlreadyRunningWarning) { From 3f438e99b1adcab35e615b483ccdc4b27cd12bee Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 14:18:14 +0100 Subject: [PATCH 24/30] add exception handling to jsonmanager save --- Bloxstrap/JsonManager.cs | 16 +++++++++++++++- Bloxstrap/Resources/Strings.Designer.cs | 9 +++++++++ Bloxstrap/Resources/Strings.resx | 3 +++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/JsonManager.cs b/Bloxstrap/JsonManager.cs index 8e786be..58a1e25 100644 --- a/Bloxstrap/JsonManager.cs +++ b/Bloxstrap/JsonManager.cs @@ -60,7 +60,21 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}..."); Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!); - File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true })); + + try + { + File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true })); + } + catch (IOException ex) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to save"); + App.Logger.WriteException(LOG_IDENT, ex); + + string errorMessage = string.Format(Resources.Strings.Bootstrapper_JsonManagerSaveFailed, ClassName, ex.Message); + Frontend.ShowMessageBox(errorMessage, System.Windows.MessageBoxImage.Warning); + + return; + } App.Logger.WriteLine(LOG_IDENT, "Save complete!"); } diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 7911c5b..e78fd90 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -170,6 +170,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Failed to save {0}: {1}. + /// + public static string Bootstrapper_JsonManagerSaveFailed { + get { + return ResourceManager.GetString("Bootstrapper.JsonManagerSaveFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Bloxstrap does not have enough disk space to download and install Roblox. Please free up some disk space and try again.. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 0a04ef2..942a932 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1192,4 +1192,7 @@ Please manually delete Bloxstrap.exe from the install location or try restarting Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option. + + Failed to save {0}: {1} + \ No newline at end of file From 15904ff51f870b74e8a9ae62bf86217ab3cab109 Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 18:51:32 +0100 Subject: [PATCH 25/30] removed unused variable --- Bloxstrap/Utilities.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 327f734..6d16a27 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -50,7 +50,6 @@ namespace Bloxstrap public static string GetRobloxVersion(bool studio) { - string versionGuid = studio ? App.State.Prop.Studio.VersionGuid : App.State.Prop.Player.VersionGuid; string fileName = studio ? "Studio/RobloxStudioBeta.exe" : "Player/RobloxPlayerBeta.exe"; string playerLocation = Path.Combine(Paths.Roblox, fileName); From 2c3a4b4e5e4de819fa96820e9dc77adc35a1d780 Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 18:52:02 +0100 Subject: [PATCH 26/30] remove unused value in activity watcher --- Bloxstrap/Integrations/ActivityWatcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index 673e195..b1d13d4 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -36,7 +36,6 @@ public event EventHandler? OnAppClose; public event EventHandler? OnRPCMessage; - private readonly Dictionary GeolocationCache = new(); private DateTime LastRPCRequest; public string LogLocation = null!; From 7e8b699adf7fe24262eb6297b3b0806b918d4a4a Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 18:55:43 +0100 Subject: [PATCH 27/30] remove unnecessary directory check --- Bloxstrap/Bootstrapper.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 62116b9..8cd32f4 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -753,8 +753,7 @@ namespace Bloxstrap List modFolderFiles = new(); - if (!Directory.Exists(Paths.Modifications)) - Directory.CreateDirectory(Paths.Modifications); + Directory.CreateDirectory(Paths.Modifications); // check custom font mod // instead of replacing the fonts themselves, we'll just alter the font family manifests From 724677be6442259fad520be3129433990be7fde4 Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:07:43 +0100 Subject: [PATCH 28/30] remove win32 message boxes --- Bloxstrap/UI/Frontend.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/Bloxstrap/UI/Frontend.cs b/Bloxstrap/UI/Frontend.cs index f1dfab2..21cca14 100644 --- a/Bloxstrap/UI/Frontend.cs +++ b/Bloxstrap/UI/Frontend.cs @@ -17,20 +17,7 @@ namespace Bloxstrap.UI if (App.LaunchSettings.QuietFlag.Active) return defaultResult; - if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None) - return ShowFluentMessageBox(message, icon, buttons); - - switch (App.Settings.Prop.BootstrapperStyle) - { - case BootstrapperStyle.FluentDialog: - case BootstrapperStyle.ClassicFluentDialog: - case BootstrapperStyle.FluentAeroDialog: - case BootstrapperStyle.ByfronDialog: - return ShowFluentMessageBox(message, icon, buttons); - - default: - return MessageBox.Show(message, App.ProjectName, buttons, icon); - } + return ShowFluentMessageBox(message, icon, buttons); } public static void ShowPlayerErrorDialog(bool crash = false) From ab8034c13b76b4d9f7bf59cee90e5de973267a1f Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:16:38 +0100 Subject: [PATCH 29/30] add string to translations --- Bloxstrap/Resources/Strings.Designer.cs | 9 +++++++++ Bloxstrap/Resources/Strings.resx | 3 +++ Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 7911c5b..e8df8ff 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -1520,6 +1520,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Are you sure you want to cancel the installation?. + /// + public static string Installer_ShouldCancel { + get { + return ResourceManager.GetString("Installer.ShouldCancel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Bloxstrap Installer. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 0a04ef2..f0f1a4f 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1192,4 +1192,7 @@ Please manually delete Bloxstrap.exe from the install location or try restarting Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option. + + Are you sure you want to cancel the installation? + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs index 90108be..49e262c 100644 --- a/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs +++ b/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs @@ -9,6 +9,7 @@ using Bloxstrap.UI.Elements.Installer.Pages; using Bloxstrap.UI.Elements.Base; using System.Windows.Media.Animation; using System.Reflection.Metadata.Ecma335; +using Bloxstrap.Resources; namespace Bloxstrap.UI.Elements.Installer { @@ -102,7 +103,7 @@ namespace Bloxstrap.UI.Elements.Installer if (Finished) return; - var result = Frontend.ShowMessageBox("Are you sure you want to cancel the installation?", MessageBoxImage.Warning, MessageBoxButton.YesNo); + var result = Frontend.ShowMessageBox(Strings.Installer_ShouldCancel, MessageBoxImage.Warning, MessageBoxButton.YesNo); if (result != MessageBoxResult.Yes) e.Cancel = true; From d8b259ace6d018fbac19870842ce8927c3842c0d Mon Sep 17 00:00:00 2001 From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:17:36 +0100 Subject: [PATCH 30/30] fix indentation --- Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs index f235af0..fa2b984 100644 --- a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs @@ -108,7 +108,7 @@ namespace Bloxstrap.UI.ViewModels.Settings string path = new RobloxPlayerData().ExecutablePath; if (File.Exists(path)) - PInvoke.SHObjectProperties(HWND.Null, SHOP_TYPE.SHOP_FILEPATH, path, "Compatibility"); + PInvoke.SHObjectProperties(HWND.Null, SHOP_TYPE.SHOP_FILEPATH, path, "Compatibility"); else Frontend.ShowMessageBox(Strings.Common_RobloxNotInstalled, MessageBoxImage.Error);