diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml index 28b450e..8f4642f 100644 --- a/Bloxstrap/App.xaml +++ b/Bloxstrap/App.xaml @@ -3,7 +3,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Bloxstrap" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" - ShutdownMode="OnExplicitShutdown"> + ShutdownMode="OnExplicitShutdown" + DispatcherUnhandledException="GlobalExceptionHandler"> diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 8368835..2954958 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -2,17 +2,21 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Net.Http; using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows; +using System.Windows.Threading; + using Microsoft.Win32; using Bloxstrap.Dialogs; using Bloxstrap.Enums; using Bloxstrap.Helpers; +using Bloxstrap.Integrations; using Bloxstrap.Models; using Bloxstrap.Views; @@ -77,35 +81,25 @@ namespace Bloxstrap Logger.Initialize(Path.Combine(logdir, $"{ProjectName}_{timestamp}.log")); } + void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + + Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread"); + Logger.WriteLine($"[App::OnStartup] {e.Exception}"); + + if (!IsQuiet) + Settings.Prop.BootstrapperStyle.GetNew().ShowError($"{e.Exception.GetType()}: {e.Exception.Message}"); + + Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); + } + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}"); - // todo: remove this once 32-bit support is fully gone - if (!App.IsQuiet && !Environment.Is64BitOperatingSystem) - { - string message = "In the near future, Roblox will no longer support 32-bit Windows devices. To keep playing Roblox, please use a device that is 64-bit compatible."; - - // check if the processor actually supports 64-bit with wmic - // chances are the user just has a 32-bit version of windows installed on 64-bit hardware - Process p = new(); - p.StartInfo.CreateNoWindow = true; - p.StartInfo.UseShellExecute = false; - p.StartInfo.RedirectStandardOutput = true; - p.StartInfo.FileName = "wmic.exe"; - p.StartInfo.Arguments = "cpu get Architecture"; - p.Start(); - - // https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-processor - // architecture type 9 is x64 - if (p.StandardOutput.ReadToEnd().Contains('9')) - message += "\n\nYour computer is running a 32-bit version of Windows but is actually 64-bit compatible. Search online for how to upgrade to a 64-bit version of Windows."; - - ShowMessageBox(message, MessageBoxImage.Warning); - } - // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); @@ -148,6 +142,29 @@ namespace Bloxstrap } } + // todo: remove this once 32-bit support is fully gone + if (!App.IsQuiet && !Environment.Is64BitOperatingSystem) + { + string message = "In the near future, Roblox will no longer support 32-bit Windows devices. To keep playing Roblox, please use a device that is 64-bit compatible."; + + // check if the processor actually supports 64-bit with wmic + // chances are the user just has a 32-bit version of windows installed on 64-bit hardware + Process p = new(); + p.StartInfo.CreateNoWindow = true; + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "wmic.exe"; + p.StartInfo.Arguments = "cpu get Architecture"; + p.Start(); + + // https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-processor + // architecture type 9 is x64 + if (p.StandardOutput.ReadToEnd().Contains('9')) + message += "\n\nYour computer is running a 32-bit version of Windows but is actually 64-bit compatible. Search online for how to upgrade to a 64-bit version of Windows."; + + ShowMessageBox(message, MessageBoxImage.Warning); + } + // check if installed using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}")) { @@ -158,12 +175,10 @@ namespace Bloxstrap BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName); InitLog(); - // we have reshade enabled by default so we need this - FastFlags.SetRenderingMode("Direct3D 11"); - if (!IsQuiet) { IsSetupComplete = false; + FastFlags.Load(); new MainWindow().ShowDialog(); } } @@ -182,7 +197,7 @@ namespace Bloxstrap } Directories.Initialize(BaseDirectory); - FastFlags.AltFileLocation = Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json"); + FastFlags.Load(); // we shouldn't save settings on the first run until the first installation is finished, // just in case the user decides to cancel the install @@ -192,151 +207,140 @@ namespace Bloxstrap Settings.Load(); State.Load(); } + #if !DEBUG - try - { - if (!IsUninstall && !IsFirstRun) - Updater.CheckInstalledVersion(); + if (!IsUninstall && !IsFirstRun) + Updater.CheckInstalledVersion(); #endif - string commandLine = ""; + string commandLine = ""; - if (IsMenuLaunch) + if (IsMenuLaunch) + { + Mutex mutex; + + try { - Mutex mutex; - - try - { - mutex = Mutex.OpenExisting("Bloxstrap_MenuMutex"); - Logger.WriteLine("[App::OnStartup] Bloxstrap_MenuMutex mutex exists, aborting menu launch..."); - Terminate(); - } - catch - { - // no mutex exists, continue to opening preferences menu - mutex = new(true, "Bloxstrap_MenuMutex"); - } - - if (Utilities.GetProcessCount(ProjectName) > 1) - ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); - - new MainWindow().ShowDialog(); - FastFlags.Save(); + mutex = Mutex.OpenExisting("Bloxstrap_MenuMutex"); + Logger.WriteLine("[App::OnStartup] Bloxstrap_MenuMutex mutex exists, aborting menu launch..."); + Terminate(); } - else if (LaunchArgs.Length > 0) + catch { - if (LaunchArgs[0].StartsWith("roblox-player:")) - { - commandLine = Protocol.ParseUri(LaunchArgs[0]); - } - else if (LaunchArgs[0].StartsWith("roblox:")) - { - commandLine = $"--app --deeplink {LaunchArgs[0]}"; - } - else - { - commandLine = "--app"; - } + // no mutex exists, continue to opening preferences menu + mutex = new(true, "Bloxstrap_MenuMutex"); + } + + if (Utilities.GetProcessCount(ProjectName) > 1) + ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); + + new MainWindow().ShowDialog(); + } + else if (LaunchArgs.Length > 0) + { + if (LaunchArgs[0].StartsWith("roblox-player:")) + { + commandLine = Protocol.ParseUri(LaunchArgs[0]); + } + else if (LaunchArgs[0].StartsWith("roblox:")) + { + commandLine = $"--app --deeplink {LaunchArgs[0]}"; } else { commandLine = "--app"; } - - if (!String.IsNullOrEmpty(commandLine)) - { - if (!IsFirstRun) - ShouldSaveConfigs = true; - - DeployManager.Channel = Settings.Prop.Channel; - - // start bootstrapper and show the bootstrapper modal if we're not running silently - Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper"); - Bootstrapper bootstrapper = new(commandLine); - IBootstrapperDialog? dialog = null; - - if (!IsQuiet) - { - Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog"); - dialog = Settings.Prop.BootstrapperStyle.GetNew(); - bootstrapper.Dialog = dialog; - dialog.Bootstrapper = bootstrapper; - } - - // handle roblox singleton mutex for multi-instance launching - // note we're handling it here in the main thread and NOT in the - // bootstrapper as handling mutexes in async contexts suuuuuucks - - Mutex? singletonMutex = null; - - if (Settings.Prop.MultiInstanceLaunching) - { - Logger.WriteLine("[App::OnStartup] Creating singleton mutex"); - - try - { - Mutex.OpenExisting("ROBLOX_singletonMutex"); - Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!"); - } - catch - { - // create the singleton mutex before the game client does - singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); - } - } - - // there's a bug here that i have yet to fix! - // sometimes the task just terminates when the bootstrapper hasn't - // actually finished, causing the bootstrapper to hang indefinitely - // i have no idea how the fuck this happens, but it happens like VERY - // rarely so i'm not too concerned by it - // maybe one day ill find out why it happens - Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => - { - Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished"); - - if (t.IsFaulted) - Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper"); - - if (t.Exception is null) - return; - - Logger.WriteLine($"[App::OnStartup] {t.Exception}"); - -#if DEBUG - throw t.Exception; -#else - var exception = t.Exception.InnerExceptions.Count >= 1 ? t.Exception.InnerExceptions[0] : t.Exception; - dialog?.ShowError($"{exception.GetType()}: {exception.Message}"); -#endif - }); - - dialog?.ShowBootstrapper(); - bootstrapperTask.Wait(); - - if (singletonMutex is not null) - { - Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed"); - - // we've got ownership of the roblox singleton mutex! - // if we stop running, everything will screw up once any more roblox instances launched - while (Utilities.GetProcessCount("RobloxPlayerBeta", false) != 0) - { - Thread.Sleep(5000); - } - } - } -#if !DEBUG } - catch (Exception ex) + else { - Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread"); - Logger.WriteLine($"[App::OnStartup] {ex}"); + commandLine = "--app"; + } + + if (!String.IsNullOrEmpty(commandLine)) + { + if (!IsFirstRun) + ShouldSaveConfigs = true; + + DeployManager.Channel = Settings.Prop.Channel; + + if (Settings.Prop.UseReShade) + ReShade.CheckRobloxReleaseChannel().Wait(); + + // start bootstrapper and show the bootstrapper modal if we're not running silently + Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper"); + Bootstrapper bootstrapper = new(commandLine); + IBootstrapperDialog? dialog = null; if (!IsQuiet) - Settings.Prop.BootstrapperStyle.GetNew().ShowError($"{ex.GetType()}: {ex.Message}"); - } + { + Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog"); + dialog = Settings.Prop.BootstrapperStyle.GetNew(); + bootstrapper.Dialog = dialog; + dialog.Bootstrapper = bootstrapper; + } + + // handle roblox singleton mutex for multi-instance launching + // note we're handling it here in the main thread and NOT in the + // bootstrapper as handling mutexes in async contexts suuuuuucks + + Mutex? singletonMutex = null; + + if (Settings.Prop.MultiInstanceLaunching) + { + Logger.WriteLine("[App::OnStartup] Creating singleton mutex"); + + try + { + Mutex.OpenExisting("ROBLOX_singletonMutex"); + Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!"); + } + catch + { + // create the singleton mutex before the game client does + singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); + } + } + + // there's a bug here that i have yet to fix! + // sometimes the task just terminates when the bootstrapper hasn't + // actually finished, causing the bootstrapper to hang indefinitely + // i have no idea how the fuck this happens, but it happens like VERY + // rarely so i'm not too concerned by it + // maybe one day ill find out why it happens + Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => + { + Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished"); + + if (t.IsFaulted) + Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper"); + + if (t.Exception is null) + return; + + Logger.WriteLine($"[App::OnStartup] {t.Exception}"); + +#if DEBUG + throw t.Exception; +#else + var exception = t.Exception.InnerExceptions.Count >= 1 ? t.Exception.InnerExceptions[0] : t.Exception; + dialog?.ShowError($"{exception.GetType()}: {exception.Message}"); + Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); #endif + }); + + dialog?.ShowBootstrapper(); + bootstrapperTask.Wait(); + + if (singletonMutex is not null) + { + Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed"); + + // we've got ownership of the roblox singleton mutex! + // if we stop running, everything will screw up once any more roblox instances launched + while (Process.GetProcessesByName("RobloxPlayerBeta").Any()) + Thread.Sleep(5000); + } + } Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating..."); diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index c6ebe34..c1d35ef 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.1.1 - 2.1.1.0 + 2.2.0 + 2.2.0.0 app.manifest diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 3e019e7..63bfa48 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -62,24 +62,25 @@ namespace Bloxstrap }; private const string AppSettings = - "\n" + - "\n" + - " content\n" + - " http://www.roblox.com\n" + - "\n"; + "\r\n" + + "\r\n" + + " content\r\n" + + " http://www.roblox.com\r\n" + + "\r\n"; private readonly CancellationTokenSource _cancelTokenSource = new(); private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); - private static bool ShouldInstallWebView2 = false; private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); + private static bool ShouldInstallWebView2 = false; - private string? _launchCommandLine; + private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); + + private string _launchCommandLine; private string _latestVersionGuid = null!; private PackageManifest _versionPackageManifest = null!; private string _versionFolder = null!; - private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); private bool _isInstalling = false; private double _progressIncrement; @@ -91,7 +92,7 @@ namespace Bloxstrap #endregion #region Core - public Bootstrapper(string? launchCommandLine = null) + public Bootstrapper(string launchCommandLine) { _launchCommandLine = launchCommandLine; @@ -116,6 +117,19 @@ namespace Bloxstrap Dialog.Message = message; } + private void UpdateProgressbar() + { + int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); + + // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 + // too lazy to fix properly so lol + if (newProgress > 100) + return; + + if (Dialog is not null) + Dialog.ProgressValue = newProgress; + } + public async Task Run() { App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); @@ -172,10 +186,9 @@ namespace Bloxstrap _versionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); if (App.IsFirstRun) - { App.ShouldSaveConfigs = true; - App.FastFlags.Save(); - } + + MigrateIntegrations(); if (ShouldInstallWebView2) await InstallWebView2(); @@ -185,6 +198,7 @@ namespace Bloxstrap await ReShade.CheckModifications(); + App.FastFlags.Save(); await ApplyModifications(); if (App.IsFirstRun || FreshInstall) @@ -192,8 +206,6 @@ namespace Bloxstrap CheckInstall(); - await RbxFpsUnlocker.CheckInstall(); - // at this point we've finished updating our configs App.Settings.Save(); App.State.Save(); @@ -207,60 +219,6 @@ namespace Bloxstrap await StartRoblox(); } - private async Task CheckForUpdates() - { - // don't update if there's another instance running (likely running in the background) - if (Utilities.GetProcessCount(App.ProjectName) > 1) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); - return; - } - - string currentVersion = $"{App.ProjectName} v{App.Version}"; - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); - - var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); - - if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) - { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); - return; - } - - SetStatus($"Getting the latest {App.ProjectName}..."); - - // 64-bit is always the first option - GithubReleaseAsset asset = releaseInfo.Assets[Environment.Is64BitOperatingSystem ? 0 : 1]; - string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); - - if (!File.Exists(downloadLocation)) - { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - - await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); - } - - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); - - ProcessStartInfo startInfo = new() - { - FileName = downloadLocation, - }; - - foreach (string arg in App.LaunchArgs) - startInfo.ArgumentList.Add(arg); - - App.Settings.Save(); - - Process.Start(startInfo); - - Environment.Exit(0); - } - private async Task CheckLatestVersion() { SetStatus("Connecting to Roblox..."); @@ -271,6 +229,179 @@ namespace Bloxstrap _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); } + private async Task StartRoblox() + { + SetStatus("Starting Roblox..."); + + if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) + { + Utilities.OpenWebsite("https://www.roblox.com/games"); + Dialog?.CloseBootstrapper(); + return; + } + + _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); + + if (App.Settings.Prop.Channel.ToLower() != DeployManager.DefaultChannel.ToLower()) + _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLower(); + + // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes + bool shouldWait = false; + + // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now + int gameClientPid; + using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) + { + gameClientPid = gameClient.Id; + } + + List autocloseProcesses = new(); + GameActivityWatcher? activityWatcher = null; + DiscordRichPresence? richPresence = null; + ServerNotifier? serverNotifier = null; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); + + using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) + { + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (!startEventFired) + return; + } + + if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) + { + activityWatcher = new(); + shouldWait = true; + } + + if (App.Settings.Prop.UseDiscordRichPresence) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); + richPresence = new(activityWatcher!); + } + + if (App.Settings.Prop.ShowServerDetails) + { + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); + serverNotifier = new(activityWatcher!); + } + + // launch custom integrations now + foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); + + try + { + Process process = Process.Start(integration.Location, integration.LaunchArgs); + + if (integration.AutoClose) + { + shouldWait = true; + autocloseProcesses.Add(process); + } + } + catch (Exception ex) + { + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); + } + } + + // event fired, wait for 3 seconds then close + await Task.Delay(3000); + Dialog?.CloseBootstrapper(); + + // keep bloxstrap open in the background if needed + if (!shouldWait) + return; + + activityWatcher?.StartWatcher(); + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); + + while (Process.GetProcesses().Any(x => x.Id == gameClientPid)) + await Task.Delay(1000); + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox has exited"); + + richPresence?.Dispose(); + + foreach (Process process in autocloseProcesses) + { + if (process.HasExited) + continue; + + App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); + process.Kill(); + } + } + + public void CancelInstall() + { + if (!_isInstalling) + { + App.Terminate(ERROR_INSTALL_USEREXIT); + return; + } + + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); + + _cancelTokenSource.Cancel(); + _cancelFired = true; + + try + { + // clean up install + if (App.IsFirstRun) + Directory.Delete(Directories.Base, true); + else if (Directory.Exists(_versionFolder)) + Directory.Delete(_versionFolder, true); + } + catch (Exception ex) + { + App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); + App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); + } + + App.Terminate(ERROR_INSTALL_USEREXIT); + } + #endregion + + #region App Install + public static void Register() + { + using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) + { + applicationKey.SetValue("InstallLocation", Directories.Base); + } + + // set uninstall key + using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) + { + uninstallKey.SetValue("DisplayIcon", $"{Directories.Application},0"); + uninstallKey.SetValue("DisplayName", App.ProjectName); + uninstallKey.SetValue("DisplayVersion", App.Version); + + if (uninstallKey.GetValue("InstallDate") is null) + uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); + + uninstallKey.SetValue("InstallLocation", Directories.Base); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); + uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); + uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + } + + App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); + } + private void CheckInstallMigration() { // check if we've changed our install location since the last time we started @@ -334,194 +465,6 @@ namespace Bloxstrap App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); } - private async Task StartRoblox() - { - string startEventName = App.ProjectName.Replace(" ", "") + "StartEvent"; - - SetStatus("Starting Roblox..."); - - if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) - { - Utilities.OpenWebsite("https://www.roblox.com/games"); - Dialog?.CloseBootstrapper(); - return; - } - - // launch time isn't really required for all launches, but it's usually just safest to do this - _launchCommandLine += " --launchtime=" + DateTimeOffset.Now.ToUnixTimeMilliseconds(); - - if (App.Settings.Prop.Channel.ToLower() != DeployManager.DefaultChannel.ToLower()) - _launchCommandLine += " -channel " + App.Settings.Prop.Channel.ToLower(); - - _launchCommandLine += " -startEvent " + startEventName; - - // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes - bool shouldWait = false; - - Process gameClient = Process.Start(_playerLocation, _launchCommandLine); - List autocloseProcesses = new(); - GameActivityWatcher? activityWatcher = null; - DiscordRichPresence? richPresence = null; - ServerNotifier? serverNotifier = null; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClient.Id})"); - - using (SystemEvent startEvent = new(startEventName)) - { - bool startEventFired = await startEvent.WaitForEvent(); - - startEvent.Close(); - - if (!startEventFired) - return; - } - - if (App.Settings.Prop.RFUEnabled && Process.GetProcessesByName(RbxFpsUnlocker.ApplicationName).Length == 0) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using rbxfpsunlocker"); - - ProcessStartInfo startInfo = new() - { - WorkingDirectory = Path.Combine(Directories.Integrations, "rbxfpsunlocker"), - FileName = Path.Combine(Directories.Integrations, @"rbxfpsunlocker\rbxfpsunlocker.exe") - }; - - Process process = Process.Start(startInfo)!; - - if (App.Settings.Prop.RFUAutoclose) - { - shouldWait = true; - autocloseProcesses.Add(process); - } - } - - if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails) - { - activityWatcher = new(); - shouldWait = true; - } - - if (App.Settings.Prop.UseDiscordRichPresence) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); - richPresence = new(activityWatcher!); - } - - if (App.Settings.Prop.ShowServerDetails) - { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier"); - serverNotifier = new(activityWatcher!); - } - - // launch custom integrations now - foreach (CustomIntegration integration in App.Settings.Prop.CustomIntegrations) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); - - try - { - Process process = Process.Start(integration.Location, integration.LaunchArgs); - - if (integration.AutoClose) - { - shouldWait = true; - autocloseProcesses.Add(process); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); - } - } - - // event fired, wait for 3 seconds then close - await Task.Delay(3000); - Dialog?.CloseBootstrapper(); - - // keep bloxstrap open in the background if needed - if (!shouldWait) - return; - - activityWatcher?.StartWatcher(); - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); - await gameClient.WaitForExitAsync(); - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Roblox exited with code {gameClient.ExitCode}"); - - richPresence?.Dispose(); - - foreach (Process process in autocloseProcesses) - { - if (process.HasExited) - continue; - - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); - process.Kill(); - } - } - - public void CancelInstall() - { - if (!_isInstalling) - { - App.Terminate(ERROR_INSTALL_USEREXIT); - return; - } - - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); - - _cancelTokenSource.Cancel(); - _cancelFired = true; - - try - { - // clean up install - if (App.IsFirstRun) - Directory.Delete(Directories.Base, true); - else if (Directory.Exists(_versionFolder)) - Directory.Delete(_versionFolder, true); - } - catch (Exception ex) - { - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Could not fully clean up installation!"); - App.Logger.WriteLine($"[Bootstrapper::CancelInstall] {ex}"); - } - - App.Terminate(ERROR_INSTALL_USEREXIT); - } -#endregion - - #region App Install - public static void Register() - { - using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) - { - applicationKey.SetValue("InstallLocation", Directories.Base); - } - - // set uninstall key - using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) - { - uninstallKey.SetValue("DisplayIcon", $"{Directories.Application},0"); - uninstallKey.SetValue("DisplayName", App.ProjectName); - uninstallKey.SetValue("DisplayVersion", App.Version); - - if (uninstallKey.GetValue("InstallDate") is null) - uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); - - uninstallKey.SetValue("InstallLocation", Directories.Base); - uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", "pizzaboxer"); - uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); - uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); - uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); - uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); - uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); - } - - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Registered application"); - } - public static void CheckInstall() { App.Logger.WriteLine("[Bootstrapper::StartRoblox] Checking install"); @@ -581,6 +524,60 @@ namespace Bloxstrap } } + private async Task CheckForUpdates() + { + // don't update if there's another instance running (likely running in the background) + if (Utilities.GetProcessCount(App.ProjectName) > 1) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); + return; + } + + string currentVersion = $"{App.ProjectName} v{App.Version}"; + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for {App.ProjectName} updates..."); + + var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); + + if (releaseInfo?.Assets is null || currentVersion == releaseInfo.Name) + { + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + return; + } + + SetStatus($"Getting the latest {App.ProjectName}..."); + + // 64-bit is always the first option + GithubReleaseAsset asset = releaseInfo.Assets[Environment.Is64BitOperatingSystem ? 0 : 1]; + string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.Name}..."); + + if (!File.Exists(downloadLocation)) + { + var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + + await using var fileStream = new FileStream(downloadLocation, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream); + } + + App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.Name}..."); + + ProcessStartInfo startInfo = new() + { + FileName = downloadLocation, + }; + + foreach (string arg in App.LaunchArgs) + startInfo.ArgumentList.Add(arg); + + App.Settings.Save(); + + Process.Start(startInfo); + + Environment.Exit(0); + } + private void Uninstall() { // prompt to shutdown roblox if its currently running @@ -653,22 +650,9 @@ namespace Bloxstrap Dialog?.ShowSuccess($"{App.ProjectName} has succesfully uninstalled"); } -#endregion + #endregion #region Roblox Install - private void UpdateProgressbar() - { - int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); - - // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 - // too lazy to fix properly so lol - if (newProgress > 100) - return; - - if (Dialog is not null) - Dialog.ProgressValue = newProgress; - } - private async Task InstallLatestVersion() { _isInstalling = true; @@ -819,6 +803,15 @@ namespace Bloxstrap App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); } + public static void MigrateIntegrations() + { + // v2.2.0 - remove rbxfpsunlocker + string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); + + if (Directory.Exists(rbxfpsunlocker)) + Directory.Delete(rbxfpsunlocker, true); + } + private async Task ApplyModifications() { SetStatus("Applying Roblox modifications..."); @@ -914,7 +907,7 @@ namespace Bloxstrap try { - packageDirectory = PackageDirectories.First(x => x.Key != "RobloxApp.zip" && fileLocation.StartsWith(x.Value)); + packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); } catch (InvalidOperationException) { @@ -1102,6 +1095,6 @@ namespace Bloxstrap entry.ExtractToFile(fileLocation); } -#endregion + #endregion } } diff --git a/Bloxstrap/Helpers/DeployManager.cs b/Bloxstrap/Helpers/DeployManager.cs index 87030ae..c87534d 100644 --- a/Bloxstrap/Helpers/DeployManager.cs +++ b/Bloxstrap/Helpers/DeployManager.cs @@ -76,35 +76,17 @@ namespace Bloxstrap.Helpers } } - // basically any channel that has had a deploy within the past month with a windowsplayer build - public static readonly List ChannelsAbstracted = new() + // most commonly used/interesting channels + public static readonly List SelectableChannels = new() { "LIVE", - "ZNext", - "ZCanary", - "ZIntegration" - }; - - // why not? - public static readonly List ChannelsAll = new() - { - "LIVE", - "ZAvatarTeam", - "ZAvatarRelease", - "ZCanary", - "ZCanary1", - "ZCanary2", - "ZCanary3", - "ZCanaryApps", + "ZWinPlayer64", "ZFlag", - "ZIntegration", - "ZIntegration1", - "ZLive", - "ZLive1", "ZNext", - "ZSocialTeam", - "ZStudioInt1", - "ZStudioInt2" + "ZCanary", + "ZIntegration", + "ZAvatarTeam", + "ZSocialTeam" }; #endregion @@ -112,7 +94,7 @@ namespace Bloxstrap.Helpers { App.Logger.WriteLine($"[DeployManager::GetLastDeploy] Getting deploy info for channel {Channel} (timestamp={timestamp})"); - HttpResponseMessage deployInfoResponse = await App.HttpClient.GetAsync($"https://clientsettings.roblox.com/v2/client-version/WindowsPlayer/channel/{Channel}"); + HttpResponseMessage deployInfoResponse = await App.HttpClient.GetAsync($"https://clientsettings.roblox.com/v2/client-version/WindowsPlayer/channel/{Channel}").ConfigureAwait(false); string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync(); diff --git a/Bloxstrap/Helpers/FastFlagManager.cs b/Bloxstrap/Helpers/FastFlagManager.cs index 3fb2da5..ce87d3e 100644 --- a/Bloxstrap/Helpers/FastFlagManager.cs +++ b/Bloxstrap/Helpers/FastFlagManager.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.IO; using System.Text.Json; @@ -15,7 +16,7 @@ namespace Bloxstrap.Helpers public Dictionary Changes = new(); // only one missing here is Metal because lol - public static IReadOnlyDictionary RenderingModes { get; set; } = new Dictionary() + public static IReadOnlyDictionary RenderingModes => new Dictionary { { "Automatic", "" }, { "Direct3D 11", "FFlagDebugGraphicsPreferD3D11" }, @@ -23,10 +24,63 @@ namespace Bloxstrap.Helpers { "Vulkan", "FFlagDebugGraphicsPreferVulkan" } }; + // this is one hell of a variable definition lmao + public static IReadOnlyDictionary> IGMenuVersions => new Dictionary> + { + { + "Default", + new Dictionary + { + { "FFlagDisableNewIGMinDUA", null }, + { "FFlagEnableInGameMenuV3", null } + } + }, + + { + "Version 1 (2015)", + new Dictionary + { + { "FFlagDisableNewIGMinDUA", "True" }, + { "FFlagEnableInGameMenuV3", "False" } + } + }, + + { + "Version 2 (2020)", + new Dictionary + { + { "FFlagDisableNewIGMinDUA", "False" }, + { "FFlagEnableInGameMenuV3", "False" } + } + }, + + { + "Version 3 (2021)", + new Dictionary + { + { "FFlagDisableNewIGMinDUA", "False" }, + { "FFlagEnableInGameMenuV3", "True" } + } + } + }; + + // all fflags are stored as strings + // to delete a flag, set the value as null + public void SetValue(string key, object? value) + { + if (value is null) + { + Changes[key] = null; + App.Logger.WriteLine($"[FastFlagManager::SetValue] Deletion of '{key}' is pending"); + } + else + { + Changes[key] = value.ToString(); + App.Logger.WriteLine($"[FastFlagManager::SetValue] Value change for '{key}' to '{value}' is pending"); + } + } + // this returns null if the fflag doesn't exist - // this also returns as a string because deserializing an object doesn't - // deserialize back into the original object type, it instead deserializes - // as a "JsonElement" which is annoying public string? GetValue(string key) { // check if we have an updated change for it pushed first @@ -44,26 +98,39 @@ namespace Bloxstrap.Helpers foreach (var mode in RenderingModes) { if (mode.Key != "Automatic") - App.FastFlags.Changes[mode.Value] = null; + SetValue(mode.Value, null); } if (value != "Automatic") - App.FastFlags.Changes[RenderingModes[value]] = true; + SetValue(RenderingModes[value], "True"); + } + + public override void Load() + { + base.Load(); + + // set to 9999 by default if it doesnt already exist + if (GetValue("DFIntTaskSchedulerTargetFps") is null) + SetValue("DFIntTaskSchedulerTargetFps", 9999); + + // reshade / exclusive fullscreen requires direct3d 11 to work + if (GetValue(RenderingModes["Direct3D 11"]) != "True" && (App.Settings.Prop.UseReShade || App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False")) + SetRenderingMode("Direct3D 11"); } public override void Save() { App.Logger.WriteLine($"[FastFlagManager::Save] Attempting to save JSON to {FileLocation}..."); + // reload for any changes made while the menu was open + Load(); + if (Changes.Count == 0) { App.Logger.WriteLine($"[FastFlagManager::Save] No changes to apply, aborting."); return; } - // reload for any changes made while the menu was open - Load(); - foreach (var change in Changes) { if (change.Value is null) @@ -73,7 +140,7 @@ namespace Bloxstrap.Helpers continue; } - App.Logger.WriteLine($"[FastFlagManager::Save] Setting '{change.Key}' to {change.Value}"); + App.Logger.WriteLine($"[FastFlagManager::Save] Setting '{change.Key}' to '{change.Value}'"); Prop[change.Key] = change.Value; } diff --git a/Bloxstrap/Helpers/JsonManager.cs b/Bloxstrap/Helpers/JsonManager.cs index fb59f7d..ef39f9c 100644 --- a/Bloxstrap/Helpers/JsonManager.cs +++ b/Bloxstrap/Helpers/JsonManager.cs @@ -1,32 +1,27 @@ -using Bloxstrap.Models; -using Bloxstrap.Properties; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Text.Json; -using System.Threading; namespace Bloxstrap.Helpers { public class JsonManager where T : new() { public T Prop { get; set; } = new(); - public virtual string FileLocation => AltFileLocation ?? Path.Combine(Directories.Base, $"{typeof(T).Name}.json"); - public string? AltFileLocation { get; set; } + public virtual string FileLocation => Path.Combine(Directories.Base, $"{typeof(T).Name}.json"); - public void Load() + public virtual void Load() { App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Loading JSON from {FileLocation}..."); try { T? settings = JsonSerializer.Deserialize(File.ReadAllText(FileLocation)); - Prop = settings ?? throw new ArgumentNullException("Deserialization returned null"); + + if (settings is null) + throw new ArgumentNullException("Deserialization returned null"); + + Prop = settings; + App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] JSON loaded successfully!"); } catch (Exception ex) diff --git a/Bloxstrap/Helpers/Protocol.cs b/Bloxstrap/Helpers/Protocol.cs index 8524566..70c56b3 100644 --- a/Bloxstrap/Helpers/Protocol.cs +++ b/Bloxstrap/Helpers/Protocol.cs @@ -18,7 +18,7 @@ namespace Bloxstrap.Helpers { "launchmode", "--" }, { "gameinfo", "-t " }, { "placelauncherurl", "-j "}, - // { "launchtime", "--launchtime=" }, we'll set this when launching the game client + { "launchtime", "--launchtime=" }, { "browsertrackerid", "-b " }, { "robloxLocale", "--rloc " }, { "gameLocale", "--gloc " }, @@ -50,6 +50,10 @@ namespace Bloxstrap.Helpers if (key == "placelauncherurl") val = HttpUtility.UrlDecode(val); + // we'll set this before launching because for some reason roblox just refuses to launch if its like a few minutes old so ??? + if (key == "launchtime") + val = "LAUNCHTIMEPLACEHOLDER"; + if (key == "channel") { if (val.ToLower() != App.Settings.Prop.Channel.ToLower()) diff --git a/Bloxstrap/Helpers/Updater.cs b/Bloxstrap/Helpers/Updater.cs index c36636e..c9c1ebe 100644 --- a/Bloxstrap/Helpers/Updater.cs +++ b/Bloxstrap/Helpers/Updater.cs @@ -78,14 +78,6 @@ namespace Bloxstrap.Helpers Bootstrapper.Register(); - // update check: if we're upgrading to v2.1.0 and have reshade enabled, - // we need to set our renderer to direct3d 11 - if (App.Settings.Prop.UseReShade && App.FastFlags.GetValue(FastFlagManager.RenderingModes["Direct3D 11"]) is null) - { - App.FastFlags.SetRenderingMode("Direct3D 11"); - App.FastFlags.Save(); - } - if (isAutoUpgrade) { NotifyIcon notification = new() diff --git a/Bloxstrap/Integrations/RbxFpsUnlocker.cs b/Bloxstrap/Integrations/RbxFpsUnlocker.cs deleted file mode 100644 index bb56886..0000000 --- a/Bloxstrap/Integrations/RbxFpsUnlocker.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Threading.Tasks; -using Bloxstrap.Helpers; - -using Bloxstrap.Models; - -namespace Bloxstrap.Integrations -{ - internal class RbxFpsUnlocker - { - public const string ApplicationName = "rbxfpsunlocker"; - public const string ProjectRepository = "axstin/rbxfpsunlocker"; - - // default settings but with QuickStart set to true and CheckForUpdates set to false - private static readonly string Settings = - "UnlockClient=true\n" + - "UnlockStudio=false\n" + - "FPSCapValues=[30.000000, 60.000000, 75.000000, 120.000000, 144.000000, 165.000000, 240.000000, 360.000000]\n" + - "FPSCapSelection=0\n" + - "FPSCap=0.000000\n" + - "CheckForUpdates=false\n" + - "NonBlockingErrors=true\n" + - "SilentErrors=false\n" + - "QuickStart=true\n"; - - public static void CheckIfRunning() - { - Process[] processes = Process.GetProcessesByName(ApplicationName); - - if (processes.Length == 0) - return; - - App.Logger.WriteLine("[RbxFpsUnlocker::CheckIfRunning] Closing currently running rbxfpsunlocker processes..."); - - try - { - foreach (Process process in processes) - { - if (process.MainModule?.FileName is null) - continue; - - if (!process.MainModule.FileName.Contains(Directories.Base)) - continue; - - process.Kill(); - process.Close(); - } - } - catch (Exception ex) - { - App.Logger.WriteLine($"[RbxFpsUnlocker::CheckIfRunning] Could not close rbxfpsunlocker process! {ex}"); - } - } - - public static async Task CheckInstall() - { - string folderLocation = Path.Combine(Directories.Base, "Integrations\\rbxfpsunlocker"); - string fileLocation = Path.Combine(folderLocation, "rbxfpsunlocker.exe"); - string settingsLocation = Path.Combine(folderLocation, "settings"); - - if (!App.Settings.Prop.RFUEnabled) - { - // don't delete rbxfpsunlocker if rbxfpsunlocker and roblox is currently running - if (Utilities.CheckIfProcessRunning(ApplicationName) && Utilities.CheckIfRobloxRunning()) - return; - - App.State.Prop.RbxFpsUnlockerVersion = ""; - App.State.Save(); - - if (Directory.Exists(folderLocation)) - { - CheckIfRunning(); - Directory.Delete(folderLocation, true); - } - - return; - } - - var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); - - if (releaseInfo is null || releaseInfo.Assets is null) - return; - - string downloadUrl = releaseInfo.Assets[0].BrowserDownloadUrl; - - DirectoryInfo directory = new(folderLocation); - directory.Create(); - // i have no idea how the read only flag enables itself but apparently it just does - directory.Attributes &= ~FileAttributes.ReadOnly; - - if (File.Exists(fileLocation)) - { - // no new release published, return - if (App.State.Prop.RbxFpsUnlockerVersion == releaseInfo.TagName) - return; - - CheckIfRunning(); - File.Delete(fileLocation); - } - - App.Logger.WriteLine("[RbxFpsUnlocker::CheckInstall] Installing/Updating rbxfpsunlocker..."); - - { - byte[] bytes = await App.HttpClient.GetByteArrayAsync(downloadUrl); - - using MemoryStream zipStream = new(bytes); - using ZipArchive archive = new(zipStream); - - archive.ExtractToDirectory(folderLocation, true); - } - - if (!File.Exists(settingsLocation)) - await File.WriteAllTextAsync(settingsLocation, Settings); - - App.State.Prop.RbxFpsUnlockerVersion = releaseInfo.TagName; - App.State.Save(); - } - } -} diff --git a/Bloxstrap/Integrations/ReShade.cs b/Bloxstrap/Integrations/ReShade.cs index 5931f1e..1a541aa 100644 --- a/Bloxstrap/Integrations/ReShade.cs +++ b/Bloxstrap/Integrations/ReShade.cs @@ -1,14 +1,13 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading.Tasks; -using Bloxstrap.Helpers; +using System.Windows; +using Bloxstrap.Helpers; using Bloxstrap.Models; using IniParser; @@ -501,5 +500,35 @@ namespace Bloxstrap.Integrations SynchronizeConfigFile(); } - } + + public static async Task CheckRobloxReleaseChannel() + { + App.Logger.WriteLine($"[ReShade::CheckRobloxReleaseChannel] Checking current Roblox release channel ({App.Settings.Prop.Channel})..."); + + if (App.Settings.Prop.Channel.ToLower() == DeployManager.DefaultChannel.ToLower()) + { + App.Logger.WriteLine($"[App::OnStartup] Channel is already {DeployManager.DefaultChannel}"); + return; + } + + ClientVersion versionInfo = await App.DeployManager.GetLastDeploy().ConfigureAwait(false); + string manifest = await App.HttpClient.GetStringAsync($"{App.DeployManager.BaseUrl}/{versionInfo.VersionGuid}-rbxManifest.txt"); + + if (!manifest.Contains("RobloxPlayerBeta.dll")) + return; + + MessageBoxResult result = !App.Settings.Prop.PromptChannelChange ? MessageBoxResult.Yes : App.ShowMessageBox( + $"You currently have ReShade enabled, however your current preferred channel ({App.Settings.Prop.Channel}) does not support ReShade. Would you like to switch to {DeployManager.DefaultChannel}? ", + MessageBoxImage.Question, + MessageBoxButton.YesNo + ); + + if (result != MessageBoxResult.Yes) + return; + + App.Logger.WriteLine($"[App::OnStartup] Changed Roblox build channel from {App.Settings.Prop.Channel} to {DeployManager.DefaultChannel}"); + App.DeployManager.Channel = App.Settings.Prop.Channel = DeployManager.DefaultChannel; + } + + } } diff --git a/Bloxstrap/Models/Settings.cs b/Bloxstrap/Models/Settings.cs index a8e96b6..95b1b8e 100644 --- a/Bloxstrap/Models/Settings.cs +++ b/Bloxstrap/Models/Settings.cs @@ -25,8 +25,6 @@ namespace Bloxstrap.Models // integration configuration public bool UseDiscordRichPresence { get; set; } = true; public bool HideRPCButtons { get; set; } = true; - public bool RFUEnabled { get; set; } = false; - public bool RFUAutoclose { get; set; } = false; public bool UseReShade { get; set; } = true; public bool UseReShadeExtraviPresets { get; set; } = true; public bool ShowServerDetails { get; set; } = false; diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index fcc2d7c..be8c221 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -9,7 +9,6 @@ namespace Bloxstrap.Models public class State { public string VersionGuid { get; set; } = ""; - public string RbxFpsUnlockerVersion { get; set; } = ""; public string ReShadeConfigVersion { get; set; } = ""; public string ExtraviReShadePresetsVersion { get; set; } = ""; public List ModManifest { get; set; } = new(); diff --git a/Bloxstrap/ViewModels/AppearanceViewModel.cs b/Bloxstrap/ViewModels/AppearanceViewModel.cs index 320a9c8..818085f 100644 --- a/Bloxstrap/ViewModels/AppearanceViewModel.cs +++ b/Bloxstrap/ViewModels/AppearanceViewModel.cs @@ -118,7 +118,10 @@ namespace Bloxstrap.ViewModels get => App.Settings.Prop.BootstrapperIconCustomLocation; set { + App.Settings.Prop.BootstrapperIcon = BootstrapperIcon.IconCustom; App.Settings.Prop.BootstrapperIconCustomLocation = value; + + OnPropertyChanged(nameof(Icon)); OnPropertyChanged(nameof(IconPreviewSource)); } } diff --git a/Bloxstrap/ViewModels/InstallationViewModel.cs b/Bloxstrap/ViewModels/InstallationViewModel.cs index 9a0d2b4..f72f01e 100644 --- a/Bloxstrap/ViewModels/InstallationViewModel.cs +++ b/Bloxstrap/ViewModels/InstallationViewModel.cs @@ -1,17 +1,15 @@ -using Bloxstrap.Enums; -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.Linq; -using System.Text; using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; -using System.Windows.Forms; -using Wpf.Ui.Mvvm.Interfaces; -using System.ComponentModel; using Bloxstrap.Helpers; using Bloxstrap.Models; -using System.Diagnostics; namespace Bloxstrap.ViewModels { @@ -20,14 +18,13 @@ namespace Bloxstrap.ViewModels public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private IEnumerable _channels = DeployManager.ChannelsAbstracted.Contains(App.Settings.Prop.Channel) ? DeployManager.ChannelsAbstracted : DeployManager.ChannelsAll; - private bool _showAllChannels = !DeployManager.ChannelsAbstracted.Contains(App.Settings.Prop.Channel); + private bool _manualChannelEntry = !DeployManager.SelectableChannels.Contains(App.Settings.Prop.Channel); public ICommand BrowseInstallLocationCommand => new RelayCommand(BrowseInstallLocation); public ICommand OpenFolderCommand => new RelayCommand(OpenFolder); - - public DeployInfo? ChannelDeployInfo { get; private set; } = null; //new DeployInfo(){ Version = "hi", VersionGuid = "hi", Timestamp = "January 25 2023 at 6:03:48 PM" }; + public DeployInfo? ChannelDeployInfo { get; private set; } = null; + public string ChannelInfoLoadingText { get; private set; } = null!; public InstallationViewModel() { @@ -36,20 +33,32 @@ namespace Bloxstrap.ViewModels private async Task LoadChannelDeployInfo(string channel) { + ChannelInfoLoadingText = "Fetching latest deploy info, please wait..."; + OnPropertyChanged(nameof(ChannelInfoLoadingText)); + ChannelDeployInfo = null; OnPropertyChanged(nameof(ChannelDeployInfo)); App.DeployManager.Channel = channel; - ClientVersion info = await App.DeployManager.GetLastDeploy(true); - ChannelDeployInfo = new DeployInfo + try { - Version = info.Version, - VersionGuid = info.VersionGuid, - Timestamp = info.Timestamp?.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", App.CultureFormat)! - }; + ClientVersion info = await App.DeployManager.GetLastDeploy(true); - OnPropertyChanged(nameof(ChannelDeployInfo)); + ChannelDeployInfo = new DeployInfo + { + Version = info.Version, + VersionGuid = info.VersionGuid, + Timestamp = info.Timestamp?.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", App.CultureFormat)! + }; + + OnPropertyChanged(nameof(ChannelDeployInfo)); + } + catch (Exception) + { + ChannelInfoLoadingText = "Failed to get deploy info.\nIs the channel name valid?"; + OnPropertyChanged(nameof(ChannelInfoLoadingText)); + } } private void BrowseInstallLocation() @@ -74,43 +83,36 @@ namespace Bloxstrap.ViewModels set => App.BaseDirectory = value; } - public IEnumerable Channels - { - get => _channels; - set => _channels = value; - } + public IEnumerable Channels => DeployManager.SelectableChannels; public string Channel { get => App.Settings.Prop.Channel; set { - //Task.Run(() => GetChannelInfo(value)); Task.Run(() => LoadChannelDeployInfo(value)); App.Settings.Prop.Channel = value; } } - public bool ShowAllChannels + public bool ManualChannelEntry { - get => _showAllChannels; + get => _manualChannelEntry; set { - if (value) - { - Channels = DeployManager.ChannelsAll; - } - else - { - Channels = DeployManager.ChannelsAbstracted; + _manualChannelEntry = value; + + if (!value && !Channels.Contains(Channel)) Channel = DeployManager.DefaultChannel; - OnPropertyChanged(nameof(Channel)); - } - OnPropertyChanged(nameof(Channels)); - - _showAllChannels = value; + OnPropertyChanged(nameof(Channel)); + OnPropertyChanged(nameof(ChannelComboBoxVisibility)); + OnPropertyChanged(nameof(ChannelTextBoxVisibility)); } } + + // cant use data bindings so i have to do whatever tf this is + public Visibility ChannelComboBoxVisibility => ManualChannelEntry ? Visibility.Collapsed : Visibility.Visible; + public Visibility ChannelTextBoxVisibility => ManualChannelEntry ? Visibility.Visible : Visibility.Collapsed; } } diff --git a/Bloxstrap/ViewModels/IntegrationsViewModel.cs b/Bloxstrap/ViewModels/IntegrationsViewModel.cs index e242d81..d097c9d 100644 --- a/Bloxstrap/ViewModels/IntegrationsViewModel.cs +++ b/Bloxstrap/ViewModels/IntegrationsViewModel.cs @@ -97,30 +97,13 @@ namespace Bloxstrap.ViewModels set => App.Settings.Prop.UseReShadeExtraviPresets = value; } - public bool RbxFpsUnlockerEnabled - { - get => App.Settings.Prop.RFUEnabled; - set - { - App.Settings.Prop.RFUEnabled = value; - RbxFpsUnlockerAutocloseEnabled = value; - OnPropertyChanged(nameof(RbxFpsUnlockerAutocloseEnabled)); - } - } - - public bool RbxFpsUnlockerAutocloseEnabled - { - get => App.Settings.Prop.RFUAutoclose; - set => App.Settings.Prop.RFUAutoclose = value; - } - public bool ShowServerDetailsEnabled { get => App.Settings.Prop.ShowServerDetails; - set => App.Settings.Prop.ShowServerDetails = value; + set => App.Settings.Prop.ShowServerDetails = value; } - public ObservableCollection CustomIntegrations + public ObservableCollection CustomIntegrations { get => App.Settings.Prop.CustomIntegrations; set => App.Settings.Prop.CustomIntegrations = value; diff --git a/Bloxstrap/ViewModels/MainWindowViewModel.cs b/Bloxstrap/ViewModels/MainWindowViewModel.cs index 261548a..259c3f8 100644 --- a/Bloxstrap/ViewModels/MainWindowViewModel.cs +++ b/Bloxstrap/ViewModels/MainWindowViewModel.cs @@ -60,6 +60,7 @@ namespace Bloxstrap.ViewModels if (!App.IsFirstRun) { App.ShouldSaveConfigs = true; + App.FastFlags.Save(); if (App.BaseDirectory != _originalBaseDirectory) { diff --git a/Bloxstrap/ViewModels/ModsViewModel.cs b/Bloxstrap/ViewModels/ModsViewModel.cs index 7467c80..52d21e5 100644 --- a/Bloxstrap/ViewModels/ModsViewModel.cs +++ b/Bloxstrap/ViewModels/ModsViewModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Windows.Input; @@ -14,10 +15,7 @@ namespace Bloxstrap.ViewModels public ICommand OpenModsFolderCommand => new RelayCommand(OpenModsFolder); - private void OpenModsFolder() - { - Process.Start("explorer.exe", Directories.Modifications); - } + private void OpenModsFolder() => Process.Start("explorer.exe", Directories.Modifications); public bool OldDeathSoundEnabled { @@ -37,24 +35,14 @@ namespace Bloxstrap.ViewModels set => App.Settings.Prop.UseDisableAppPatch = value; } - public IReadOnlyDictionary RenderingModes => FastFlagManager.RenderingModes; - - // this flag has to be set to false to work, weirdly enough - public bool ExclusiveFullscreenEnabled + public int FramerateLimit { - get => App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False"; - set - { - App.FastFlags.Changes["FFlagHandleAltEnterFullscreenManually"] = value ? false : null; - - if (value) - { - App.FastFlags.SetRenderingMode("Direct3D 11"); - OnPropertyChanged(nameof(SelectedRenderingMode)); - } - } + get => Int32.TryParse(App.FastFlags.GetValue("DFIntTaskSchedulerTargetFps"), out int x) ? x : 60; + set => App.FastFlags.SetValue("DFIntTaskSchedulerTargetFps", value); } + public IReadOnlyDictionary RenderingModes => FastFlagManager.RenderingModes; + public string SelectedRenderingMode { get @@ -71,6 +59,61 @@ namespace Bloxstrap.ViewModels set => App.FastFlags.SetRenderingMode(value); } + // this flag has to be set to false to work, weirdly enough + public bool ExclusiveFullscreenEnabled + { + get => App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False"; + set + { + App.FastFlags.SetValue("FFlagHandleAltEnterFullscreenManually", value ? "False" : null); + + if (value) + { + App.FastFlags.SetRenderingMode("Direct3D 11"); + OnPropertyChanged(nameof(SelectedRenderingMode)); + } + } + } + + public IReadOnlyDictionary> IGMenuVersions => FastFlagManager.IGMenuVersions; + + public string SelectedIGMenuVersion + { + get + { + // yeah this kinda sucks + foreach (var version in IGMenuVersions) + { + bool flagsMatch = true; + + foreach (var flag in version.Value) + { + if (App.FastFlags.GetValue(flag.Key) != flag.Value) + flagsMatch = false; + } + + if (flagsMatch) + return version.Key; + } + + return "Default"; + } + + set + { + foreach (var flag in IGMenuVersions[value]) + { + App.FastFlags.SetValue(flag.Key, flag.Value); + } + } + } + + public bool AlternateGraphicsSelectorEnabled + { + get => App.FastFlags.GetValue("FFlagFixGraphicsQuality") == "True"; + set => App.FastFlags.SetValue("FFlagFixGraphicsQuality", value ? "True" : null); + } + public bool DisableFullscreenOptimizationsEnabled { get => App.Settings.Prop.DisableFullscreenOptimizations; diff --git a/Bloxstrap/Views/Pages/AboutPage.xaml b/Bloxstrap/Views/Pages/AboutPage.xaml index 2a31eee..8130c33 100644 --- a/Bloxstrap/Views/Pages/AboutPage.xaml +++ b/Bloxstrap/Views/Pages/AboutPage.xaml @@ -137,12 +137,6 @@ - - - - - - diff --git a/Bloxstrap/Views/Pages/BehaviourPage.xaml b/Bloxstrap/Views/Pages/BehaviourPage.xaml index 8b677b7..35ed8ff 100644 --- a/Bloxstrap/Views/Pages/BehaviourPage.xaml +++ b/Bloxstrap/Views/Pages/BehaviourPage.xaml @@ -43,8 +43,8 @@ - - + + diff --git a/Bloxstrap/Views/Pages/InstallationPage.xaml b/Bloxstrap/Views/Pages/InstallationPage.xaml index 6142e79..a67d256 100644 --- a/Bloxstrap/Views/Pages/InstallationPage.xaml +++ b/Bloxstrap/Views/Pages/InstallationPage.xaml @@ -59,7 +59,8 @@ - + + @@ -114,10 +115,10 @@ - + - + diff --git a/Bloxstrap/Views/Pages/IntegrationsPage.xaml b/Bloxstrap/Views/Pages/IntegrationsPage.xaml index 8aebf4f..4800714 100644 --- a/Bloxstrap/Views/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/Views/Pages/IntegrationsPage.xaml @@ -34,6 +34,15 @@ + + + + + + + + + @@ -93,28 +102,6 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -127,8 +114,8 @@ - - + + diff --git a/Bloxstrap/Views/Pages/IntegrationsPage.xaml.cs b/Bloxstrap/Views/Pages/IntegrationsPage.xaml.cs index c4066ab..5809833 100644 --- a/Bloxstrap/Views/Pages/IntegrationsPage.xaml.cs +++ b/Bloxstrap/Views/Pages/IntegrationsPage.xaml.cs @@ -15,10 +15,6 @@ namespace Bloxstrap.Views.Pages { DataContext = new IntegrationsViewModel(); InitializeComponent(); - - // rbxfpsunlocker does not have 64 bit support - if (!Environment.Is64BitOperatingSystem) - this.RbxFpsUnlockerOptions.Visibility = Visibility.Collapsed; } public void CustomIntegrationSelection(object sender, SelectionChangedEventArgs e) diff --git a/Bloxstrap/Views/Pages/ModsPage.xaml b/Bloxstrap/Views/Pages/ModsPage.xaml index cf1610b..c6360f4 100644 --- a/Bloxstrap/Views/Pages/ModsPage.xaml +++ b/Bloxstrap/Views/Pages/ModsPage.xaml @@ -6,7 +6,7 @@ xmlns:models="clr-namespace:Bloxstrap.ViewModels" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800" + d:DesignHeight="800" d:DesignWidth="800" Title="ModsPage" Scrollable="True"> @@ -99,16 +99,19 @@ + + + - - + + - + @@ -119,6 +122,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/Views/Pages/ModsPage.xaml.cs b/Bloxstrap/Views/Pages/ModsPage.xaml.cs index b5c7e43..e3c3b73 100644 --- a/Bloxstrap/Views/Pages/ModsPage.xaml.cs +++ b/Bloxstrap/Views/Pages/ModsPage.xaml.cs @@ -1,5 +1,7 @@ using System; using System.Windows; +using System.Windows.Input; + using Bloxstrap.ViewModels; namespace Bloxstrap.Views.Pages @@ -11,9 +13,6 @@ namespace Bloxstrap.Views.Pages { public ModsPage() { - if (!App.IsFirstRun) - App.FastFlags.Load(); - DataContext = new ModsViewModel(); InitializeComponent(); @@ -21,5 +20,7 @@ namespace Bloxstrap.Views.Pages if (Environment.OSVersion.Version.Build < 17093) this.MiscellaneousOptions.Visibility = Visibility.Collapsed; } + + private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = !Int32.TryParse(e.Text, out int _); } } diff --git a/README.md b/README.md index a5500fe..3f4376a 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Alternatively, you can install Bloxstrap via [Winget](https://winstall.app/apps/ > winget install bloxstrap ``` -You will also need the [.NET 6 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.14-windows-x64-installer). If you don't already have it installed, you'll be prompted to install it anyway. Be sure to install Bloxstrap after you've installed this. +You will also need the [.NET 6 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.16-windows-x64-installer). If you don't already have it installed, you'll be prompted to install it anyway. Be sure to install Bloxstrap after you've installed this. It's not unlikely that Windows Smartscreen will show a popup when you run Bloxstrap for the first time. This happens because it's an unknown program, not because it's actually detected as being malicious. To dismiss it, just click on "More info" and then "Run anyway". @@ -36,12 +36,12 @@ Here's some of the features that Bloxstrap provides over the stock Roblox bootst * Persistent file modifications - re-adds the old death sound! * Support for shaders with [ReShade](https://reshade.me) and [Extravi's ReShade Presets](https://bloxshade.com/) * Painless support for Discord Rich Presence - no auth cookie needed! -* Automatic silent FPS unlocking with [rbxfpsunlocker](https://github.com/axstin/rbxfpsunlocker) -* Ability to disable the Roblox desktop app +* Built-in FPS unlocking * A customizable launcher look - includes dark theme! -* Ability to opt into non-production Roblox release channels -* Ability to see what region your current server is located in -* Support for having multiple Roblox game instances open simultaneously +* Lets you disable the Roblox desktop app +* Lets you opt into non-production Roblox release channels +* Lets you see what region your current server is located in +* Lets you have multiple Roblox game instances open simultaneously All the available features are browsable through the Bloxstrap menu. There's not too many, but it's recommended to look through all of them.