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. diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 858b85e..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) @@ -101,8 +93,7 @@ namespace Bloxstrap _showingExceptionDialog = true; - if (!LaunchSettings.QuietFlag.Active) - Frontend.ShowExceptionDialog(ex); + Frontend.ShowExceptionDialog(ex); Terminate(ErrorCode.ERROR_INSTALL_FAILURE); } @@ -110,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"); @@ -199,6 +191,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); @@ -206,21 +218,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 @@ -247,10 +244,8 @@ namespace Bloxstrap Locale.Set(Settings.Prop.Locale); -#if !DEBUG if (!LaunchSettings.BypassUpdateCheck) Installer.HandleUpgrade(); -#endif LaunchHandler.ProcessLaunchArgs(); } diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs index 5b202ab..c74003a 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 ExecutableName { get; } = null!; + + public virtual string Directory { get; } = null!; + + public string LockFilePath => Path.Combine(Directory, "Bloxstrap.lock"); + + public string ExecutablePath => Path.Combine(Directory, 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..b8a45c9 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 Directory { get; } + + string LockFilePath { 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..923c6a1 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 Directory => 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..fca5a63 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 Directory => 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..8cd32f4 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -34,57 +34,19 @@ 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 readonly IAppData AppData; 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; + + private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); + private bool _noConnection = false; public IBootstrapperDialog? Dialog = null; @@ -92,14 +54,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) @@ -125,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() { @@ -132,39 +118,15 @@ 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(); - 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!; - - 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 (connectionResult is not null) + HandleConnectionError(connectionResult); + #if !DEBUG || DEBUG_UPDATER if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) { @@ -182,8 +144,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 +155,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 +165,46 @@ namespace Bloxstrap App.State.Load(); } - await CheckLatestVersion(); + if (!_noConnection) + { + try + { + await GetLatestVersionInfo(); + } + catch (Exception ex) + { + HandleConnectionError(ex); + } + } - // 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)) - await InstallLatestVersion(); + if (!_noConnection) + { + if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) + await UpgradeRoblox(); - if (_installWebView2) - await InstallWebView2(); + // 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(); + } - await ApplyModifications(); + // check registry entries for every launch, just in case the stock bootstrapper changes it back - // TODO: move this to install/upgrade flow - if (FreshInstall) - RegisterProgramSize(); - - CheckInstall(); - - // at this point we've finished updating our configs - App.State.Save(); + if (IsStudioLaunch) + WindowsRegistry.RegisterStudio(); + else + WindowsRegistry.RegisterPlayer(); 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 +220,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; } @@ -260,15 +231,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); @@ -285,8 +255,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 +286,9 @@ namespace Bloxstrap var startInfo = new ProcessStartInfo() { - FileName = _playerLocation, + FileName = AppData.ExecutablePath, Arguments = _launchCommandLine, - WorkingDirectory = _versionFolder + WorkingDirectory = AppData.Directory }; if (_launchMode == LaunchMode.StudioAuth) @@ -340,7 +313,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 +324,9 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Start event signalled"); + if (IsStudioLaunch) + return; + var autoclosePids = new List(); // launch custom integrations now @@ -390,39 +366,45 @@ 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) { + // 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..."); + App.Logger.WriteLine(LOG_IDENT, "Cancelling launch..."); _cancelTokenSource.Cancel(); - _cancelFired = true; - try + if (_isInstalling) { - // clean up install - if (Directory.Exists(_versionFolder)) - Directory.Delete(_versionFolder, 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(); @@ -432,47 +414,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"; @@ -578,36 +519,69 @@ namespace Bloxstrap #endregion #region Roblox Install - private async Task InstallLatestVersion() + private async Task UpgradeRoblox() { - const string LOG_IDENT = "Bootstrapper::InstallLatestVersion"; - - _isInstalling = true; + 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); - Directory.CreateDirectory(Paths.Versions); + Directory.CreateDirectory(Paths.Roblox); + + 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.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(); + + 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 - string[] 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) { - 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 +591,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 @@ -629,107 +605,133 @@ namespace Bloxstrap if (package.Name == "WebView2RuntimeInstaller.zip") continue; - // 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}")); + // extract the package async immediately after download + 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); - } - + 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.Directory, "AppSettings.xml"), AppSettings); - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; - if (!FreshInstall) + if (App.State.Prop.PromptWebView2Install) { - // 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 not null || hkcuKey is not null) { - if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) + // 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) { - App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); - - try - { - File.Delete(filename); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {filename}!"); - App.Logger.WriteException(LOG_IDENT, ex); - } + App.State.Prop.PromptWebView2Install = false; } - } - - string oldVersionFolder = Path.Combine(Paths.Versions, _versionGuid); - - // move old compatibility flags for the old location - using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) - { - string oldGameClientLocation = Path.Combine(oldVersionFolder, AppData.ExecutableName); - string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); - - if (appFlags is not null) + else { - App.Logger.WriteLine(LOG_IDENT, $"Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); - appFlagsKey.SetValue(_playerLocation, appFlags); - appFlagsKey.DeleteValue(oldGameClientLocation); + App.Logger.WriteLine(LOG_IDENT, "Installing WebView2 runtime..."); + + var 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; + } + + string baseDirectory = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); + + ExtractPackage(package); + + SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); + + var startInfo = new ProcessStartInfo() + { + WorkingDirectory = baseDirectory, + FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), + Arguments = "/silent /install" + }; + + await Process.Start(startInfo)!.WaitForExitAsync(); + + App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); + + 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 hash in cachedPackageHashes) { - foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories()) + if (!allPackageHashes.Contains(hash)) { - 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 {hash}"); + try { - dir.Delete(true); + File.Delete(Path.Combine(Paths.Downloads, hash)); } catch (Exception ex) { - App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!"); 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) / 1024; + + 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"); + + App.State.Save(); + + lockFile.Delete(); if (Dialog is not null) Dialog.CancelEnabled = false; @@ -737,95 +739,12 @@ namespace Bloxstrap _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"; - - if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) - { - App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); - return; - } 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); - - 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()}'"); - - // 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); - } - - // hmm, maybe make a unified handler for this? this is just lazily copy pasted from above - - flag = " RUNASADMIN"; - appFlags = (string?)appFlagsKey.GetValue(_playerLocation); - - 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); - } - } - // handle file mods App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); @@ -834,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 @@ -848,7 +766,9 @@ namespace Bloxstrap Directory.CreateDirectory(modFontFamiliesFolder); - foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families"))) + const string path = "rbxasset://fonts/CustomFont.ttf"; + + foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(AppData.Directory, "content\\fonts\\families"))) { string jsonFilename = Path.GetFileName(jsonFilePath); string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); @@ -858,16 +778,24 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); - FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); + var fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); if (fontFamilyData is null) continue; - foreach (FontFace fontFace in fontFamilyData.Faces) - fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; + bool shouldWrite = false; - // TODO: writing on every launch is not necessary - File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); + foreach (var fontFace in fontFamilyData.Faces) + { + if (fontFace.AssetId != path) + { + fontFace.AssetId = path; + shouldWrite = true; + } + } + + if (shouldWrite) + File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); } App.Logger.WriteLine(LOG_IDENT, "End font check"); @@ -898,7 +826,7 @@ namespace Bloxstrap modFolderFiles.Add(relativeFile); string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); - string fileVersionFolder = Path.Combine(_versionFolder, relativeFile); + string fileVersionFolder = Path.Combine(AppData.Directory, relativeFile); if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) { @@ -919,20 +847,22 @@ namespace Bloxstrap // 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 - // TODO: this needs to extract the files from packages in bulk, this is way too slow + var fileRestoreMap = new Dictionary>(); + foreach (string fileLocation in App.State.Prop.ModManifest) { if (modFolderFiles.Contains(fileLocation)) continue; - var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + var packageMapEntry = AppData.PackageDirectoryMap.SingleOrDefault(x => !String.IsNullOrEmpty(x.Value) && fileLocation.StartsWith(x.Value)); + string packageName = packageMapEntry.Key; // package doesn't exist, likely mistakenly placed file - if (String.IsNullOrEmpty(package.Key)) + if (String.IsNullOrEmpty(packageName)) { App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); - string versionFileLocation = Path.Combine(_versionFolder, fileLocation); + string versionFileLocation = Path.Combine(AppData.Directory, fileLocation); if (File.Exists(versionFileLocation)) File.Delete(versionFileLocation); @@ -940,11 +870,25 @@ namespace Bloxstrap continue; } - // restore original file - string fileName = fileLocation.Substring(package.Value.Length); - await ExtractFileFromPackage(package.Key, fileName); + string fileName = fileLocation.Substring(packageMapEntry.Value.Length); - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}"); + 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; @@ -957,18 +901,17 @@ namespace Bloxstrap { string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}"; - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) 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)) { - FileInfo file = new(packageLocation); + var file = new FileInfo(package.DownloadPath); - string calculatedMD5 = MD5Hash.FromFile(packageLocation); + string calculatedMD5 = MD5Hash.FromFile(package.DownloadPath); if (calculatedMD5 != package.Signature) { @@ -991,7 +934,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(); @@ -999,9 +942,12 @@ 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. + // 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 +956,7 @@ namespace Bloxstrap for (int i = 1; i <= maxTries; i++) { - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) return; int totalBytesRead = 0; @@ -1019,11 +965,11 @@ 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) { - if (_cancelFired) + if (_cancelTokenSource.IsCancellationRequested) { stream.Close(); fileStream.Close(); @@ -1061,6 +1007,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 ); @@ -1069,8 +1016,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(); @@ -1087,47 +1034,31 @@ namespace Bloxstrap } } - private Task ExtractPackage(Package package) + private void ExtractPackage(Package package, List? files = null) { const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - if (_cancelFired) - return Task.CompletedTask; + string packageFolder = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); + string? fileFilter = null; - string packageLocation = Path.Combine(Paths.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name]); + // 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}"); - - _packagesExtracted += 1; - - return Task.CompletedTask; } - - 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/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/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; } 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/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/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/Installer.cs b/Bloxstrap/Installer.cs index e807455..9e2f6f2 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 @@ -63,10 +75,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 +88,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"); } @@ -92,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; @@ -162,11 +178,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 +196,10 @@ namespace Bloxstrap ); if (result != MessageBoxResult.OK) + { App.Terminate(ErrorCode.ERROR_CANCELLED); + return; + } try { @@ -207,44 +227,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 { @@ -261,8 +275,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) @@ -272,8 +288,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) }); } @@ -383,15 +398,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)) @@ -512,8 +542,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"); @@ -526,6 +555,10 @@ namespace Bloxstrap App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null); } + + App.FastFlags.SetValue("FFlagFixGraphicsQuality", null); + + Directory.Delete(Path.Combine(Paths.Base, "Versions")); } App.Settings.Save(); diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index 15d9676..7816afd 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 { @@ -40,7 +38,6 @@ namespace Bloxstrap.Integrations public event EventHandler? OnAppClose; public event EventHandler? OnRPCMessage; - private readonly Dictionary GeolocationCache = new(); private DateTime LastRPCRequest; public string LogLocation = null!; 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/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 0c00a4c..891a7ed 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); @@ -134,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 { @@ -169,15 +177,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 +194,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/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; 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/RobloxApi/UserInfoResponse.cs b/Bloxstrap/Models/APIs/Roblox/UserInfoResponse.cs similarity index 100% rename from Bloxstrap/Models/RobloxApi/UserInfoResponse.cs rename to Bloxstrap/Models/APIs/Roblox/UserInfoResponse.cs diff --git a/Bloxstrap/Models/ActivityData.cs b/Bloxstrap/Models/Entities/ActivityData.cs similarity index 70% rename from Bloxstrap/Models/ActivityData.cs rename to Bloxstrap/Models/Entities/ActivityData.cs index fdc1520..dbbae3d 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,21 +28,21 @@ 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 AccessCode { get; set; } = string.Empty; - public string MachineAddress { get; set; } = String.Empty; - public string UserId { 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; } @@ -54,15 +54,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(); @@ -73,6 +73,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}"; @@ -82,37 +84,32 @@ namespace Bloxstrap.Models else deeplink += "&gameInstanceId=" + JobId; - if (launchData && !String.IsNullOrEmpty(RPCLaunchData)) + if (launchData && !string.IsNullOrEmpty(RPCLaunchData)) deeplink += "&launchData=" + HttpUtility.UrlEncode(RPCLaunchData); 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"); + var ipInfo = await Http.GetJson($"https://ipinfo.io/{MachineAddress}/json"); - GlobalCache.PendingTasks.Add(MachineAddress, ipInfoTask); - - var ipInfo = await ipInfoTask; - - GlobalCache.PendingTasks.Remove(MachineAddress); - - if (String.IsNullOrEmpty(ipInfo.City)) + if (string.IsNullOrEmpty(ipInfo.City)) throw new InvalidHTTPResponseException("Reported city was blank"); if (ipInfo.City == ipInfo.Region) @@ -121,26 +118,33 @@ 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}"; 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/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/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}"; 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/Persistable/AppState.cs b/Bloxstrap/Models/Persistable/AppState.cs new file mode 100644 index 0000000..57c3224 --- /dev/null +++ b/Bloxstrap/Models/Persistable/AppState.cs @@ -0,0 +1,11 @@ +namespace Bloxstrap.Models.Persistable +{ + 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/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs similarity index 93% rename from Bloxstrap/Models/Settings.cs rename to Bloxstrap/Models/Persistable/Settings.cs index bb032dc..0371483 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 { @@ -26,6 +26,5 @@ namespace Bloxstrap.Models // mod preset configuration public bool UseDisableAppPatch { get; set; } = false; - public bool DisableFullscreenOptimizations { get; set; } = false; } } diff --git a/Bloxstrap/Models/Persistable/State.cs b/Bloxstrap/Models/Persistable/State.cs new file mode 100644 index 0000000..de05265 --- /dev/null +++ b/Bloxstrap/Models/Persistable/State.cs @@ -0,0 +1,17 @@ +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(); + + public List ModManifest { get; set; } = new(); + } +} diff --git a/Bloxstrap/Models/Persistable/WindowState.cs b/Bloxstrap/Models/Persistable/WindowState.cs new file mode 100644 index 0000000..90bf334 --- /dev/null +++ b/Bloxstrap/Models/Persistable/WindowState.cs @@ -0,0 +1,13 @@ +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/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/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/Models/State.cs b/Bloxstrap/Models/State.cs deleted file mode 100644 index 49ece20..0000000 --- a/Bloxstrap/Models/State.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Bloxstrap.Models -{ - public class State - { - 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 List ModManifest { get; set; } = new(); - } -} 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/Paths.cs b/Bloxstrap/Paths.cs index 43d4a1c..36f8136 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -20,8 +20,8 @@ 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; } = ""; public static string Application { get; private set; } = ""; @@ -35,8 +35,8 @@ 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"); Application = Path.Combine(Base, $"{App.ProjectName}.exe"); } diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index a21f089..5279440 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 { @@ -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.. /// @@ -197,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.. /// @@ -539,6 +521,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. /// @@ -575,6 +566,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. /// @@ -828,6 +828,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 +892,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 { @@ -1410,6 +1473,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.. /// @@ -1455,6 +1529,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. /// @@ -2270,24 +2353,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. /// @@ -2789,6 +2854,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.... /// @@ -2880,9 +2963,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 c60c4fb..57ddfd4 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 @@ -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. @@ -1108,9 +1102,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. @@ -1175,6 +1167,41 @@ 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. + + + 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. + + + 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. + + + 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. + + + Are you sure you want to cancel the installation? + + + Failed to save {0}: {1} diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index 37784a5..e2022c1 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); @@ -32,14 +33,14 @@ { 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". 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 +67,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 +127,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 +141,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/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/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/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}"; } diff --git a/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/ConnectivityDialog.xaml index 5ad5299..03817bb 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,9 +30,9 @@ - + - + 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/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 @@ - + 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; diff --git a/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Settings/MainWindow.xaml.cs index 59bd1db..fda449f 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.Persistable.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(); 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/Elements/Settings/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml index 1e8931b..8be8d71 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml @@ -96,7 +96,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml index bb0e06f..79e6627 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/ModsPage.xaml @@ -25,18 +25,27 @@ + + + + + + + + + @@ -93,13 +102,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/Frontend.cs b/Bloxstrap/UI/Frontend.cs index 1699b6b..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) @@ -49,17 +36,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(); }); } 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/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(); } } diff --git a/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs b/Bloxstrap/UI/ViewModels/ContextMenu/ServerInformationViewModel.cs index ebf1384..8e1eccd 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!; @@ -31,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)); } 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/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/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 diff --git a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs index 226c441..78b007c 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() @@ -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)); } diff --git a/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/ModsViewModel.cs index f36b897..fa2b984 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"); @@ -96,10 +103,15 @@ namespace Bloxstrap.UI.ViewModels.Settings public FontModPresetTask TextFontTask { get; } = new(); - public bool DisableFullscreenOptimizations + private void OpenCompatSettings() { - get => App.Settings.Prop.DisableFullscreenOptimizations; - set => App.Settings.Prop.DisableFullscreenOptimizations = value; + 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); + } } } diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 4613d01..6d16a27 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Security.Principal; namespace Bloxstrap { @@ -51,10 +50,9 @@ namespace Bloxstrap public static string GetRobloxVersion(bool studio) { - string versionGuid = studio ? App.State.Prop.StudioVersionGuid : App.State.Prop.PlayerVersionGuid; - 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 ""; 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/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"); 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; }