diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 13ed82b1..cfd4ed9b 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -2,7 +2,8 @@ using System.Windows; using System.Windows.Threading; -using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.Foundation; namespace Bloxstrap { @@ -17,10 +18,13 @@ public partial class App : Application // used only for communicating between app and menu - use Directories.Base for anything else public static string BaseDirectory = null!; + public static string? CustomFontLocation; public static bool ShouldSaveConfigs { get; set; } = false; + public static bool IsSetupComplete { get; set; } = true; - public static bool IsFirstRun { get; private set; } = true; + public static bool IsFirstRun { get; set; } = true; + public static bool IsQuiet { get; private set; } = false; public static bool IsUninstall { get; private set; } = false; public static bool IsNoLaunch { get; private set; } = false; @@ -39,7 +43,11 @@ public partial class App : Application public static readonly JsonManager State = new(); public static readonly FastFlagManager FastFlags = new(); - public static readonly HttpClient HttpClient = new(new HttpClientLoggingHandler(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All })); + public static readonly HttpClient HttpClient = new( + new HttpClientLoggingHandler( + new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All } + ) + ); public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) { @@ -51,7 +59,7 @@ public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) int exitCodeNum = (int)exitCode; - Logger.WriteLine($"[App::Terminate] Terminating with exit code {exitCodeNum} ({exitCode})"); + Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); Settings.Save(); State.Save(); @@ -64,14 +72,16 @@ void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs { e.Handled = true; - Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread"); - Logger.WriteLine($"[App::OnStartup] {e.Exception}"); + Logger.WriteLine("App::GlobalExceptionHandler", "An exception occurred"); FinalizeExceptionHandling(e.Exception); } - void FinalizeExceptionHandling(Exception exception) + public static void FinalizeExceptionHandling(Exception exception, bool log = true) { + if (log) + Logger.WriteException("App::FinalizeExceptionHandling", exception); + #if DEBUG throw exception; #else @@ -84,14 +94,18 @@ void FinalizeExceptionHandling(Exception exception) protected override void OnStartup(StartupEventArgs e) { + const string LOG_IDENT = "App::OnStartup"; + base.OnStartup(e); - Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}"); + Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}"); if (String.IsNullOrEmpty(BuildMetadata.CommitHash)) - Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}"); + Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}"); else - Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})"); + Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})"); + + Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}"); // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. @@ -99,79 +113,75 @@ protected override void OnStartup(StartupEventArgs e) LaunchArgs = e.Args; - HttpClient.Timeout = TimeSpan.FromMinutes(5); + HttpClient.Timeout = TimeSpan.FromSeconds(30); HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); if (LaunchArgs.Length > 0) { if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1) { - Logger.WriteLine("[App::OnStartup] Started with IsMenuLaunch flag"); + Logger.WriteLine(LOG_IDENT, "Started with IsMenuLaunch flag"); IsMenuLaunch = true; } if (Array.IndexOf(LaunchArgs, "-quiet") != -1) { - Logger.WriteLine("[App::OnStartup] Started with IsQuiet flag"); + Logger.WriteLine(LOG_IDENT, "Started with IsQuiet flag"); IsQuiet = true; } if (Array.IndexOf(LaunchArgs, "-uninstall") != -1) { - Logger.WriteLine("[App::OnStartup] Started with IsUninstall flag"); + Logger.WriteLine(LOG_IDENT, "Started with IsUninstall flag"); IsUninstall = true; } if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1) { - Logger.WriteLine("[App::OnStartup] Started with IsNoLaunch flag"); + Logger.WriteLine(LOG_IDENT, "Started with IsNoLaunch flag"); IsNoLaunch = true; } if (Array.IndexOf(LaunchArgs, "-upgrade") != -1) { - Logger.WriteLine("[App::OnStartup] Bloxstrap started with IsUpgrade flag"); + Logger.WriteLine(LOG_IDENT, "Bloxstrap started with IsUpgrade flag"); IsUpgrade = true; } } - // check if installed - using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}")) + if (!IsMenuLaunch) { - string? installLocation = null; - - if (registryKey is not null) - installLocation = (string?)registryKey.GetValue("InstallLocation"); + Logger.WriteLine(LOG_IDENT, "Performing connectivity check..."); - if (registryKey is null || installLocation is null) + try { - Logger.WriteLine("[App::OnStartup] Running first-time install"); - - BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName); - Logger.Initialize(true); - - if (!IsQuiet) - { - IsSetupComplete = false; - FastFlags.Load(); - Controls.ShowMenu(); - } + HttpClient.GetAsync("https://detectportal.firefox.com").Wait(); + Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); } - else + catch (Exception ex) { - IsFirstRun = false; - BaseDirectory = installLocation; + Logger.WriteLine(LOG_IDENT, "Connectivity check failed!"); + Logger.WriteException(LOG_IDENT, ex); + + if (ex.GetType() == typeof(AggregateException)) + ex = ex.InnerException!; + + Controls.ShowConnectivityDialog( + "the internet", + $"Something may be preventing {ProjectName} from connecting to the internet, or you are currently offline. Please check and try again.", + ex + ); + + Terminate(ErrorCode.ERROR_CANCELLED); } } - - // exit if we don't click the install button on installation - if (!IsSetupComplete) + + using (var checker = new InstallChecker()) { - Logger.WriteLine("[App::OnStartup] Installation cancelled!"); - Terminate(ErrorCode.ERROR_CANCELLED); + checker.Check(); } - Directories.Initialize(BaseDirectory); + Paths.Initialize(BaseDirectory); // we shouldn't save settings on the first run until the first installation is finished, // just in case the user decides to cancel the install @@ -181,7 +191,7 @@ protected override void OnStartup(StartupEventArgs e) if (!Logger.Initialized) { - Logger.WriteLine("[App::OnStartup] Possible duplicate launch detected, terminating."); + Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating."); Terminate(); } @@ -195,7 +205,7 @@ protected override void OnStartup(StartupEventArgs e) #if !DEBUG if (!IsUninstall && !IsFirstRun) - Updater.CheckInstalledVersion(); + InstallChecker.CheckUpgrade(); #endif string commandLine = ""; @@ -206,9 +216,9 @@ protected override void OnStartup(StartupEventArgs e) if (menuProcess is not null) { - IntPtr handle = menuProcess.MainWindowHandle; - Logger.WriteLine($"[App::OnStartup] Found an already existing menu window with handle {handle}"); - NativeMethods.SetForegroundWindow(handle); + var handle = menuProcess.MainWindowHandle; + Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}"); + PInvoke.SetForegroundWindow((HWND)handle); } else { @@ -253,13 +263,13 @@ protected override void OnStartup(StartupEventArgs e) ShouldSaveConfigs = true; // start bootstrapper and show the bootstrapper modal if we're not running silently - Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper"); + Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); Bootstrapper bootstrapper = new(commandLine); IBootstrapperDialog? dialog = null; if (!IsQuiet) { - Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog"); + Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog"); dialog = Settings.Prop.BootstrapperStyle.GetNew(); bootstrapper.Dialog = dialog; dialog.Bootstrapper = bootstrapper; @@ -273,12 +283,12 @@ protected override void OnStartup(StartupEventArgs e) if (Settings.Prop.MultiInstanceLaunching) { - Logger.WriteLine("[App::OnStartup] Creating singleton mutex"); + Logger.WriteLine(LOG_IDENT, "Creating singleton mutex"); try { Mutex.OpenExisting("ROBLOX_singletonMutex"); - Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!"); + Logger.WriteLine(LOG_IDENT, "Warning - singleton mutex already exists!"); } catch { @@ -287,22 +297,20 @@ protected override void OnStartup(StartupEventArgs e) } } - Task bootstrapperTask = Task.Run(() => bootstrapper.Run()); - - bootstrapperTask.ContinueWith(t => + Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => { - Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished"); + Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); // notifyicon is blocking main thread, must be disposed here NotifyIcon?.Dispose(); if (t.IsFaulted) - Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper"); + Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); if (t.Exception is null) return; - Logger.WriteLine($"[App::OnStartup] {t.Exception}"); + Logger.WriteException(LOG_IDENT, t.Exception); Exception exception = t.Exception; @@ -311,7 +319,7 @@ protected override void OnStartup(StartupEventArgs e) exception = t.Exception.InnerException!; #endif - FinalizeExceptionHandling(exception); + FinalizeExceptionHandling(exception, false); }); // this ordering is very important as all wpf windows are shown as modal dialogs, mess it up and you'll end up blocking input to one of them @@ -320,13 +328,13 @@ protected override void OnStartup(StartupEventArgs e) if (!IsNoLaunch && Settings.Prop.EnableActivityTracking) NotifyIcon?.InitializeContextMenu(); - Logger.WriteLine($"[App::OnStartup] Waiting for bootstrapper task to finish"); + Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish"); 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"); + Logger.WriteLine(LOG_IDENT, "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 @@ -335,7 +343,7 @@ protected override void OnStartup(StartupEventArgs e) } } - Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating..."); + Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating..."); Terminate(); } diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index d7399537..2257c751 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.4.0 - 2.4.0.0 + 2.5.0 + 2.5.0.0 app.manifest @@ -39,7 +39,11 @@ - + + + + all + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 6824e312..024fa9b7 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -52,7 +52,7 @@ public class Bootstrapper private readonly CancellationTokenSource _cancelTokenSource = new(); private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid); - private static string DesktopShortcutLocation => Path.Combine(Directories.Desktop, "Play Roblox.lnk"); + private static string DesktopShortcutLocation => Path.Combine(Paths.Desktop, "Play Roblox.lnk"); private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"); @@ -79,7 +79,7 @@ public Bootstrapper(string launchCommandLine) private void SetStatus(string message) { - App.Logger.WriteLine($"[Bootstrapper::SetStatus] {message}"); + App.Logger.WriteLine("Bootstrapper::SetStatus", message); // yea idk if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) @@ -104,7 +104,9 @@ private void UpdateProgressBar() public async Task Run() { - App.Logger.WriteLine("[Bootstrapper::Run] Running bootstrapper"); + const string LOG_IDENT = "Bootstrapper::Run"; + + App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper"); if (App.IsUninstall) { @@ -125,7 +127,7 @@ public async Task Run() try { Mutex.OpenExisting("Bloxstrap_BootstrapperMutex").Close(); - App.Logger.WriteLine("[Bootstrapper::Run] Bloxstrap_BootstrapperMutex mutex exists, waiting..."); + App.Logger.WriteLine(LOG_IDENT, "Bloxstrap_BootstrapperMutex mutex exists, waiting..."); mutexExists = true; } catch (Exception) @@ -187,15 +189,58 @@ private async Task CheckLatestVersion() { SetStatus("Connecting to Roblox..."); - ClientVersion clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + ClientVersion clientVersion; + + try + { + clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + } + catch (Exception ex) + { + string message = "It's possible that Roblox is being blocked by a firewall. Please check and try again."; + + if (ex.GetType() == typeof(HttpResponseException)) + message = "Roblox may be down right now. See status.roblox.com for more information. Please try again later."; + + Controls.ShowConnectivityDialog("Roblox", message, ex); + + App.Terminate(ErrorCode.ERROR_CANCELLED); + return; + } + + if (clientVersion.IsBehindDefaultChannel) + { + MessageBoxResult action = App.Settings.Prop.ChannelChangeMode switch + { + ChannelChangeMode.Prompt => Controls.ShowMessageBox( + $"The channel you're currently on ({App.Settings.Prop.Channel}) is out of date, and appears to no longer be receiving updates.\n" + + $"Would you like to switch to the default channel ({RobloxDeployment.DefaultChannel})?", + MessageBoxImage.Warning, + MessageBoxButton.YesNo + ), + ChannelChangeMode.Automatic => MessageBoxResult.Yes, + ChannelChangeMode.Ignore => MessageBoxResult.No, + _ => MessageBoxResult.None + }; + + if (action == MessageBoxResult.Yes) + { + App.Logger.WriteLine("Bootstrapper::CheckLatestVersion", $"Changed Roblox channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); + + App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; + clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + } + } _latestVersionGuid = clientVersion.VersionGuid; - _versionFolder = Path.Combine(Directories.Versions, _latestVersionGuid); + _versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid); _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); } private async Task StartRoblox() { + const string LOG_IDENT = "Bootstrapper::StartRoblox"; + SetStatus("Starting Roblox..."); if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) @@ -205,13 +250,16 @@ private async Task StartRoblox() return; } - if (!File.Exists("C:\\Windows\\System32\\mfplat.dll")) + if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll"))) { Controls.ShowMessageBox( "Roblox requires the use of Windows Media Foundation components. You appear to be missing them, likely because you are using an N edition of Windows. Please install them first, and then launch Roblox.", MessageBoxImage.Error ); - Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a"); + + if (!App.IsQuiet) + Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a"); + Dialog?.CloseBootstrapper(); return; } @@ -232,10 +280,10 @@ private async Task StartRoblox() } List autocloseProcesses = new(); - RobloxActivity? activityWatcher = null; + ActivityWatcher? activityWatcher = null; DiscordRichPresence? richPresence = null; - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClientPid})"); + App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid})"); using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) { @@ -256,7 +304,7 @@ private async Task StartRoblox() if (App.Settings.Prop.UseDiscordRichPresence) { - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence"); + App.Logger.WriteLine(LOG_IDENT, "Using Discord Rich Presence"); richPresence = new(activityWatcher); App.NotifyIcon?.SetRichPresenceHandler(richPresence); @@ -266,7 +314,7 @@ private async Task StartRoblox() // 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})"); + App.Logger.WriteLine(LOG_IDENT, $"Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); try { @@ -280,7 +328,8 @@ private async Task StartRoblox() } catch (Exception ex) { - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Failed to launch integration '{integration.Name}'! ({ex.Message})"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!"); + App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}"); } } @@ -294,12 +343,12 @@ private async Task StartRoblox() activityWatcher?.StartWatcher(); - App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close"); + App.Logger.WriteLine(LOG_IDENT, "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"); + App.Logger.WriteLine(LOG_IDENT, $"Roblox has exited"); richPresence?.Dispose(); @@ -308,20 +357,22 @@ private async Task StartRoblox() if (process.HasExited) continue; - App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Autoclosing process '{process.ProcessName}' (PID {process.Id})"); + App.Logger.WriteLine(LOG_IDENT, $"Autoclosing process '{process.ProcessName}' (PID {process.Id})"); process.Kill(); } } public void CancelInstall() { + const string LOG_IDENT = "Bootstrapper::CancelInstall"; + if (!_isInstalling) { App.Terminate(ErrorCode.ERROR_CANCELLED); return; } - App.Logger.WriteLine("[Bootstrapper::CancelInstall] Cancelling install..."); + App.Logger.WriteLine(LOG_IDENT, "Cancelling install..."); _cancelTokenSource.Cancel(); _cancelFired = true; @@ -330,14 +381,14 @@ public void CancelInstall() { // clean up install if (App.IsFirstRun) - Directory.Delete(Directories.Base, true); + Directory.Delete(Paths.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.Logger.WriteLine(LOG_IDENT, "Could not fully clean up installation!"); + App.Logger.WriteException(LOG_IDENT, ex); } App.Terminate(ErrorCode.ERROR_CANCELLED); @@ -347,37 +398,41 @@ public void CancelInstall() #region App Install public static void Register() { + const string LOG_IDENT = "Bootstrapper::Register"; + using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) { - applicationKey.SetValue("InstallLocation", Directories.Base); + applicationKey.SetValue("InstallLocation", Paths.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("DisplayIcon", $"{Paths.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("InstallLocation", Paths.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("ModifyPath", $"\"{Paths.Application}\" -menu"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Paths.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"); + App.Logger.WriteLine(LOG_IDENT, "Registered application"); } public void RegisterProgramSize() { - App.Logger.WriteLine("[Bootstrapper::RegisterProgramSize] Registering approximate program size..."); + 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}"); @@ -386,11 +441,13 @@ public void RegisterProgramSize() uninstallKey.SetValue("EstimatedSize", totalSize); - App.Logger.WriteLine($"[Bootstrapper::RegisterProgramSize] Registered as {totalSize} KB"); + App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); } private void CheckInstallMigration() { + const string LOG_IDENT = "Bootstrapper::CheckInstallMigration"; + // check if we've changed our install location since the last time we started // in which case, we'll have to copy over all our folders so we don't lose any mods and stuff @@ -398,19 +455,19 @@ private void CheckInstallMigration() string? oldInstallLocation = (string?)applicationKey?.GetValue("OldInstallLocation"); - if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Directories.Base) + if (applicationKey is null || oldInstallLocation is null || oldInstallLocation == Paths.Base) return; SetStatus("Migrating install location..."); if (Directory.Exists(oldInstallLocation)) { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Moving all files in {oldInstallLocation} to {Directories.Base}..."); + App.Logger.WriteLine(LOG_IDENT, $"Moving all files in {oldInstallLocation} to {Paths.Base}..."); foreach (string oldFileLocation in Directory.GetFiles(oldInstallLocation, "*.*", SearchOption.AllDirectories)) { string relativeFile = oldFileLocation.Substring(oldInstallLocation.Length + 1); - string newFileLocation = Path.Combine(Directories.Base, relativeFile); + string newFileLocation = Path.Combine(Paths.Base, relativeFile); string? newDirectory = Path.GetDirectoryName(newFileLocation); try @@ -422,26 +479,26 @@ private void CheckInstallMigration() } catch (Exception ex) { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to move {oldFileLocation} to {newFileLocation}! {ex}"); } } try { Directory.Delete(oldInstallLocation, true); - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Deleted old install location"); + App.Logger.WriteLine(LOG_IDENT, "Deleted old install location"); } catch (Exception ex) { - App.Logger.WriteLine($"[Bootstrapper::CheckInstallMigration] Failed to delete old install location! {ex}"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to delete old install location! {ex}"); } } applicationKey.DeleteValue("OldInstallLocation"); // allow shortcuts to be re-registered - if (Directory.Exists(Directories.StartMenu)) - Directory.Delete(Directories.StartMenu, true); + if (Directory.Exists(Paths.StartMenu)) + Directory.Delete(Paths.StartMenu, true); if (File.Exists(DesktopShortcutLocation)) { @@ -449,52 +506,54 @@ private void CheckInstallMigration() App.Settings.Prop.CreateDesktopIcon = true; } - App.Logger.WriteLine("[Bootstrapper::CheckInstallMigration] Finished migrating install location!"); + App.Logger.WriteLine(LOG_IDENT, "Finished migrating install location!"); } public static void CheckInstall() { - App.Logger.WriteLine("[Bootstrapper::CheckInstall] Checking install"); + 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", Directories.Application); - ProtocolHandler.Register("roblox-player", "Roblox", Directories.Application); + ProtocolHandler.Register("roblox", "Roblox", Paths.Application); + ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application); // in case the user is reinstalling - if (File.Exists(Directories.Application) && App.IsFirstRun) - File.Delete(Directories.Application); + if (File.Exists(Paths.Application) && App.IsFirstRun) + File.Delete(Paths.Application); // check to make sure bootstrapper is in the install folder - if (!File.Exists(Directories.Application) && Environment.ProcessPath is not null) - File.Copy(Environment.ProcessPath, Directories.Application); + if (!File.Exists(Paths.Application) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Paths.Application); // this SHOULD go under Register(), // but then people who have Bloxstrap v1.0.0 installed won't have this without a reinstall // maybe in a later version? - if (!Directory.Exists(Directories.StartMenu)) + if (!Directory.Exists(Paths.StartMenu)) { - Directory.CreateDirectory(Directories.StartMenu); + Directory.CreateDirectory(Paths.StartMenu); - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); + ShellLink.Shortcut.CreateShortcut(Paths.Application, "", Paths.Application, 0) + .WriteToFile(Path.Combine(Paths.StartMenu, "Play Roblox.lnk")); - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) - .WriteToFile(Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk")); + ShellLink.Shortcut.CreateShortcut(Paths.Application, "-menu", Paths.Application, 0) + .WriteToFile(Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk")); } else { // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" - string oldMenuShortcut = Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk"); - string newMenuShortcut = Path.Combine(Directories.StartMenu, $"{App.ProjectName} Menu.lnk"); + string oldMenuShortcut = Path.Combine(Paths.StartMenu, $"Configure {App.ProjectName}.lnk"); + string newMenuShortcut = Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk"); if (File.Exists(oldMenuShortcut)) File.Delete(oldMenuShortcut); if (!File.Exists(newMenuShortcut)) - ShellLink.Shortcut.CreateShortcut(Directories.Application, "-menu", Directories.Application, 0) + ShellLink.Shortcut.CreateShortcut(Paths.Application, "-menu", Paths.Application, 0) .WriteToFile(newMenuShortcut); } @@ -504,13 +563,13 @@ public static void CheckInstall() { try { - ShellLink.Shortcut.CreateShortcut(Directories.Application, "", Directories.Application, 0) + ShellLink.Shortcut.CreateShortcut(Paths.Application, "", Paths.Application, 0) .WriteToFile(DesktopShortcutLocation); } catch (Exception ex) { - App.Logger.WriteLine("[Bootstrapper::CheckInstall] Could not create desktop shortcut, aborting"); - App.Logger.WriteLine($"[Bootstrapper::CheckInstall] {ex}"); + App.Logger.WriteLine(LOG_IDENT, "Could not create desktop shortcut, aborting"); + App.Logger.WriteException(LOG_IDENT, ex); } } @@ -521,14 +580,16 @@ public static void CheckInstall() private async Task CheckForUpdates() { + const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; + // don't update if there's another instance running (likely running in the background) if (Process.GetProcessesByName(App.ProjectName).Count() > 1) { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] More than one Bloxstrap instance running, aborting update check"); + App.Logger.WriteLine(LOG_IDENT, $"More than one Bloxstrap instance running, aborting update check"); return; } - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Checking for updates..."); + App.Logger.WriteLine(LOG_IDENT, $"Checking for updates..."); GithubRelease? releaseInfo; try @@ -537,13 +598,13 @@ private async Task CheckForUpdates() } catch (Exception ex) { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Failed to fetch releases: {ex}"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to fetch releases: {ex}"); return; } if (releaseInfo is null || releaseInfo.Assets is null) { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + App.Logger.WriteLine(LOG_IDENT, $"No updates found"); return; } @@ -552,7 +613,7 @@ private async Task CheckForUpdates() // check if we aren't using a deployed build, so we can update to one if a new version comes out if (versionComparison == 0 && App.BuildMetadata.CommitRef.StartsWith("tag") || versionComparison == 1) { - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] No updates found"); + App.Logger.WriteLine(LOG_IDENT, $"No updates found"); return; } @@ -561,9 +622,9 @@ private async Task CheckForUpdates() // 64-bit is always the first option GithubReleaseAsset asset = releaseInfo.Assets[0]; - string downloadLocation = Path.Combine(Directories.LocalAppData, "Temp", asset.Name); + string downloadLocation = Path.Combine(Paths.LocalAppData, "Temp", asset.Name); - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Downloading {releaseInfo.TagName}..."); + App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}..."); if (!File.Exists(downloadLocation)) { @@ -573,7 +634,7 @@ private async Task CheckForUpdates() await response.Content.CopyToAsync(fileStream); } - App.Logger.WriteLine($"[Bootstrapper::CheckForUpdates] Starting {releaseInfo.TagName}..."); + App.Logger.WriteLine(LOG_IDENT, $"Starting {releaseInfo.TagName}..."); ProcessStartInfo startInfo = new() { @@ -593,10 +654,12 @@ private async Task CheckForUpdates() private void Uninstall() { + const string LOG_IDENT = "Bootstrapper::Uninstall"; + // prompt to shutdown roblox if its currently running if (Process.GetProcessesByName(App.RobloxAppName).Any()) { - App.Logger.WriteLine($"[Bootstrapper::Uninstall] Prompting to shut down all open Roblox instances"); + App.Logger.WriteLine(LOG_IDENT, $"Prompting to shut down all open Roblox instances"); MessageBoxResult result = Controls.ShowMessageBox( "Roblox is currently running, but must be closed before uninstalling Bloxstrap. Would you like close Roblox now?", @@ -617,15 +680,16 @@ private void Uninstall() } catch (Exception ex) { - App.Logger.WriteLine($"[Bootstrapper::ShutdownIfRobloxRunning] Failed to close process! {ex}"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}"); } - App.Logger.WriteLine($"[Bootstrapper::Uninstall] All Roblox processes closed"); + App.Logger.WriteLine(LOG_IDENT, $"All Roblox processes closed"); } SetStatus($"Uninstalling {App.ProjectName}..."); App.ShouldSaveConfigs = false; + bool robloxStillInstalled = true; // check if stock bootstrapper is still installed RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); @@ -633,6 +697,8 @@ private void Uninstall() { ProtocolHandler.Unregister("roblox"); ProtocolHandler.Unregister("roblox-player"); + + robloxStillInstalled = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio") is not null; } else { @@ -647,31 +713,36 @@ private void Uninstall() // if the folder we're installed to does not end with "Bloxstrap", we're installed to a user-selected folder // in which case, chances are they chose to install to somewhere they didn't really mean to (prior to the added warning in 2.4.0) // if so, we're walking on eggshells and have to ensure we only clean up what we need to clean up - bool cautiousUninstall = !Directories.Base.EndsWith(App.ProjectName); + bool cautiousUninstall = !Paths.Base.EndsWith(App.ProjectName); var cleanupSequence = new List { () => Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"), - () => Directory.Delete(Directories.StartMenu, true), - () => File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")), + () => Directory.Delete(Paths.StartMenu, true), + () => File.Delete(Path.Combine(Paths.Desktop, "Play Roblox.lnk")), () => Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}") }; if (cautiousUninstall) { - cleanupSequence.Add(() => Directory.Delete(Directories.Downloads, true)); - cleanupSequence.Add(() => Directory.Delete(Directories.Modifications, true)); - cleanupSequence.Add(() => Directory.Delete(Directories.Versions, true)); - cleanupSequence.Add(() => Directory.Delete(Directories.Logs, true)); + cleanupSequence.Add(() => Directory.Delete(Paths.Downloads, true)); + cleanupSequence.Add(() => Directory.Delete(Paths.Modifications, true)); + cleanupSequence.Add(() => Directory.Delete(Paths.Versions, true)); + cleanupSequence.Add(() => Directory.Delete(Paths.Logs, true)); cleanupSequence.Add(() => File.Delete(App.Settings.FileLocation)); cleanupSequence.Add(() => File.Delete(App.State.FileLocation)); } else { - cleanupSequence.Add(() => Directory.Delete(Directories.Base, true)); + cleanupSequence.Add(() => Directory.Delete(Paths.Base, true)); } + string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox"); + + if (!robloxStillInstalled && Directory.Exists(robloxFolder)) + cleanupSequence.Add(() => Directory.Delete(robloxFolder, true)); + foreach (var process in cleanupSequence) { try @@ -680,14 +751,14 @@ private void Uninstall() } catch (Exception ex) { - App.Logger.WriteLine($"[Bootstrapper::Uninstall] Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})"); - App.Logger.WriteLine($"[Bootstrapper::Uninstall] {ex}"); + App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})"); + App.Logger.WriteException(LOG_IDENT, ex); } } Action? callback = null; - if (Directory.Exists(Directories.Base)) + if (Directory.Exists(Paths.Base)) { callback = delegate { @@ -698,9 +769,9 @@ private void Uninstall() string deleteCommand; if (cautiousUninstall) - deleteCommand = $"del /Q \"{Directories.Application}\""; + deleteCommand = $"del /Q \"{Paths.Application}\""; else - deleteCommand = $"del /Q \"{Directories.Base}\\*\" && rmdir \"{Directories.Base}\""; + deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\""; Process.Start(new ProcessStartInfo() { @@ -719,20 +790,22 @@ private void Uninstall() #region Roblox Install private async Task InstallLatestVersion() { + const string LOG_IDENT = "Bootstrapper::InstallLatestVersion"; + _isInstalling = true; SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); - Directory.CreateDirectory(Directories.Base); - Directory.CreateDirectory(Directories.Downloads); - Directory.CreateDirectory(Directories.Versions); + Directory.CreateDirectory(Paths.Base); + Directory.CreateDirectory(Paths.Downloads); + Directory.CreateDirectory(Paths.Versions); // package manifest states packed size and uncompressed size in exact bytes // packed size only matters if we don't already have the package cached on disk - string[] cachedPackages = Directory.GetFiles(Directories.Downloads); + string[] cachedPackages = Directory.GetFiles(Paths.Downloads); int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); - if (Utilities.GetFreeDiskSpace(Directories.Base) < totalSizeRequired) + if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) { Controls.ShowMessageBox( $"{App.ProjectName} does not have enough disk space to download and install Roblox. Please free up some disk space and try again.", @@ -766,7 +839,7 @@ private async Task InstallLatestVersion() // extract the package immediately after download asynchronously // discard is just used to suppress the warning - Task _ = ExtractPackage(package); + _ = ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}"); } if (_cancelFired) @@ -787,6 +860,7 @@ private async Task InstallLatestVersion() await Task.Delay(100); } + App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); await File.WriteAllTextAsync(appSettingsLocation, AppSettings); @@ -800,12 +874,12 @@ private async Task InstallLatestVersion() { if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Deleting unused package {filename}"); + App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); File.Delete(filename); } } - string oldVersionFolder = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid); + string oldVersionFolder = Path.Combine(Paths.Versions, App.State.Prop.VersionGuid); // move old compatibility flags for the old location using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) @@ -815,7 +889,7 @@ private async Task InstallLatestVersion() if (appFlags is not null) { - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); + App.Logger.WriteLine(LOG_IDENT, $"Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}..."); appFlagsKey.SetValue(_playerLocation, appFlags); appFlagsKey.DeleteValue(oldGameClientLocation); } @@ -826,12 +900,12 @@ private async Task InstallLatestVersion() // while they were launching a second instance or something idk if (!Process.GetProcessesByName(App.RobloxAppName).Any()) { - foreach (DirectoryInfo dir in new DirectoryInfo(Directories.Versions).GetDirectories()) + foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories()) { if (dir.Name == _latestVersionGuid || !dir.Name.StartsWith("version-")) continue; - App.Logger.WriteLine($"[Bootstrapper::InstallLatestVersion] Removing old version folder for {dir.Name}"); + App.Logger.WriteLine(LOG_IDENT, $"Removing old version folder for {dir.Name}"); dir.Delete(true); } } @@ -851,6 +925,8 @@ private async Task InstallLatestVersion() private async Task InstallWebView2() { + const string LOG_IDENT = "Bootstrapper::InstallWebView2"; + // check if the webview2 runtime needs to be installed // webview2 can either be installed be per-user or globally, so we need to check in both hklm and hkcu // https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#detect-if-a-suitable-webview2-runtime-is-already-installed @@ -861,7 +937,7 @@ private async Task InstallWebView2() if (hklmKey is not null || hkcuKey is not null) return; - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Installing runtime..."); + App.Logger.WriteLine(LOG_IDENT, "Installing runtime..."); string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); @@ -871,7 +947,7 @@ private async Task InstallWebView2() if (package is null) { - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); + App.Logger.WriteLine(LOG_IDENT, "Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); return; } @@ -889,20 +965,20 @@ private async Task InstallWebView2() await Process.Start(startInfo)!.WaitForExitAsync(); - App.Logger.WriteLine($"[Bootstrapper::InstallWebView2] Finished installing runtime"); + App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); } public static void MigrateIntegrations() { // v2.2.0 - remove rbxfpsunlocker - string rbxfpsunlocker = Path.Combine(Directories.Integrations, "rbxfpsunlocker"); + string rbxfpsunlocker = Path.Combine(Paths.Integrations, "rbxfpsunlocker"); if (Directory.Exists(rbxfpsunlocker)) Directory.Delete(rbxfpsunlocker, true); // v2.3.0 - remove reshade - string injectorLocation = Path.Combine(Directories.Modifications, "dxgi.dll"); - string configLocation = Path.Combine(Directories.Modifications, "ReShade.ini"); + string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll"); + string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini"); if (File.Exists(injectorLocation)) { @@ -921,10 +997,12 @@ public static void MigrateIntegrations() private async Task ApplyModifications() { + const string LOG_IDENT = "Bootstrapper::ApplyModifications"; + SetStatus("Applying Roblox modifications..."); // set executable flags for fullscreen optimizations - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking executable flags..."); + App.Logger.WriteLine(LOG_IDENT, "Checking executable flags..."); using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) { string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE"; @@ -939,7 +1017,7 @@ private async Task ApplyModifications() } else if (appFlags is not null && appFlags.Contains(flag)) { - App.Logger.WriteLine($"[Bootstrapper::ApplyModifications] Deleting flag '{flag.Trim()}'"); + 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) @@ -955,7 +1033,7 @@ private async Task ApplyModifications() if (appFlags is not null && appFlags.Contains(flag)) { - App.Logger.WriteLine($"[Bootstrapper::ApplyModifications] Deleting flag '{flag.Trim()}'"); + 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) @@ -966,21 +1044,19 @@ private async Task ApplyModifications() } // handle file mods - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Checking file mods..."); - string modFolder = Path.Combine(Directories.Modifications); + App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); // manifest has been moved to State.json - File.Delete(Path.Combine(Directories.Base, "ModManifest.txt")); + File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); List modFolderFiles = new(); - if (!Directory.Exists(modFolder)) - Directory.CreateDirectory(modFolder); + if (!Directory.Exists(Paths.Modifications)) + Directory.CreateDirectory(Paths.Modifications); bool appDisabled = App.Settings.Prop.UseDisableAppPatch && !_launchCommandLine.Contains("--deeplink"); // cursors - await CheckModPreset(App.Settings.Prop.CursorType == CursorType.From2006, new Dictionary { { @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "Cursor.From2006.ArrowCursor.png" }, @@ -1012,16 +1088,20 @@ private async Task ApplyModifications() await CheckModPreset(App.Settings.Prop.UseOldAvatarBackground && !appDisabled, @"ExtraContent\places\Mobile.rbxl", "OldAvatarBackground.rbxl"); // emoji presets are downloaded remotely from github due to how large they are - string contentFonts = Path.Combine(Directories.Modifications, "content\\fonts"); + string contentFonts = Path.Combine(Paths.Modifications, "content\\fonts"); string emojiFontLocation = Path.Combine(contentFonts, "TwemojiMozilla.ttf"); - string emojiFontHash = File.Exists(emojiFontLocation) ? Utility.MD5Hash.FromFile(emojiFontLocation) : ""; + string emojiFontHash = File.Exists(emojiFontLocation) ? MD5Hash.FromFile(emojiFontLocation) : ""; if (App.Settings.Prop.EmojiType == EmojiType.Default && EmojiTypeEx.Hashes.Values.Contains(emojiFontHash)) { + App.Logger.WriteLine(LOG_IDENT, "Reverting to default emoji font"); + File.Delete(emojiFontLocation); } else if (App.Settings.Prop.EmojiType != EmojiType.Default && emojiFontHash != App.Settings.Prop.EmojiType.GetHash()) { + App.Logger.WriteLine(LOG_IDENT, $"Configuring emoji font as {App.Settings.Prop.EmojiType}"); + if (emojiFontHash != "") File.Delete(emojiFontLocation); @@ -1035,12 +1115,17 @@ private async Task ApplyModifications() // check custom font mod // instead of replacing the fonts themselves, we'll just alter the font family manifests - string modFontFamiliesFolder = Path.Combine(Directories.Modifications, "content\\fonts\\families"); - string customFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\CustomFont.ttf"); + string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - if (File.Exists(customFontLocation)) + if (App.IsFirstRun && App.CustomFontLocation is not null) { - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] Begin font check"); + Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!); + File.Copy(App.CustomFontLocation, Paths.CustomFont); + } + + if (File.Exists(Paths.CustomFont)) + { + App.Logger.WriteLine(LOG_IDENT, "Begin font check"); Directory.CreateDirectory(modFontFamiliesFolder); @@ -1052,6 +1137,8 @@ private async Task ApplyModifications() if (File.Exists(modFilepath)) continue; + App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); + FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); if (fontFamilyData is null) @@ -1063,17 +1150,17 @@ private async Task ApplyModifications() File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); } - App.Logger.WriteLine("[Bootstrapper::ApplyModifications] End font check"); + App.Logger.WriteLine(LOG_IDENT, "End font check"); } else if (Directory.Exists(modFontFamiliesFolder)) { Directory.Delete(modFontFamiliesFolder, true); } - foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) + foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) { // get relative directory path - string relativeFile = file.Substring(modFolder.Length + 1); + string relativeFile = file.Substring(Paths.Modifications.Length + 1); // v1.7.0 - README has been moved to the preferences menu now if (relativeFile == "README.txt") @@ -1082,30 +1169,26 @@ private async Task ApplyModifications() continue; } + if (relativeFile.EndsWith(".lock")) + continue; + modFolderFiles.Add(relativeFile); - } - // copy and overwrite - foreach (string file in modFolderFiles) - { - string fileModFolder = Path.Combine(modFolder, file); - string fileVersionFolder = Path.Combine(_versionFolder, file); + string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); + string fileVersionFolder = Path.Combine(_versionFolder, relativeFile); - if (File.Exists(fileVersionFolder)) + if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) { - if (Utility.MD5Hash.FromFile(fileModFolder) == Utility.MD5Hash.FromFile(fileVersionFolder)) - continue; - } - - string? directory = Path.GetDirectoryName(fileVersionFolder); - - if (directory is null) + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); continue; + } - Directory.CreateDirectory(directory); + Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); File.Copy(fileModFolder, fileVersionFolder, true); - File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); + Filesystem.AssertReadOnly(fileVersionFolder); + + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); } // the manifest is primarily here to keep track of what files have been @@ -1116,15 +1199,13 @@ private async Task ApplyModifications() if (modFolderFiles.Contains(fileLocation)) continue; - KeyValuePair packageDirectory; + var package = PackageDirectories.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); - try - { - packageDirectory = PackageDirectories.First(x => x.Value != "" && fileLocation.StartsWith(x.Value)); - } - catch (InvalidOperationException) + // package doesn't exist, likely mistakenly placed file + if (String.IsNullOrEmpty(package.Key)) { - // package doesn't exist, likely mistakenly placed file + App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); + string versionFileLocation = Path.Combine(_versionFolder, fileLocation); if (File.Exists(versionFileLocation)) @@ -1134,17 +1215,23 @@ private async Task ApplyModifications() } // restore original file - string fileName = fileLocation.Substring(packageDirectory.Value.Length); - ExtractFileFromPackage(packageDirectory.Key, fileName); + string fileName = fileLocation.Substring(package.Value.Length); + await ExtractFileFromPackage(package.Key, fileName); + + App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}"); } App.State.Prop.ModManifest = modFolderFiles; App.State.Save(); + + App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); } private static async Task CheckModPreset(bool condition, string location, string name) { - string fullLocation = Path.Combine(Directories.Modifications, location); + string LOG_IDENT = $"Bootstrapper::CheckModPreset.{name}"; + + string fullLocation = Path.Combine(Paths.Modifications, location); string fileHash = File.Exists(fullLocation) ? MD5Hash.FromFile(fullLocation) : ""; if (!condition && fileHash == "") @@ -1155,16 +1242,28 @@ private static async Task CheckModPreset(bool condition, string location, string if (!condition) { - if (fileHash != "" && fileHash == embeddedHash) - File.Delete(fullLocation); + if (fileHash == embeddedHash) + { + App.Logger.WriteLine(LOG_IDENT, $"Deleting '{location}' as preset is disabled, and mod file matches preset"); + Filesystem.AssertReadOnly(fullLocation); + File.Delete(fullLocation); + } + return; } if (fileHash != embeddedHash) - { + { + App.Logger.WriteLine(LOG_IDENT, $"Writing '{location}' as preset is enabled, and mod file does not exist or does not match preset"); + Directory.CreateDirectory(Path.GetDirectoryName(fullLocation)!); - File.Delete(fullLocation); + + if (File.Exists(fullLocation)) + { + Filesystem.AssertReadOnly(fullLocation); + File.Delete(fullLocation); + } await File.WriteAllBytesAsync(fullLocation, embeddedData); } @@ -1178,29 +1277,33 @@ private static async Task CheckModPreset(bool condition, Dictionary= maxTries) + throw; + + _totalDownloadedBytes -= totalBytesRead; + UpdateProgressBar(); + } } } private async Task ExtractPackage(Package package) { + const string LOG_IDENT = "Bootstrapper::ExtractPackage"; + if (_cancelFired) return; - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageLocation = Path.Combine(Paths.Downloads, package.Signature); string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); - string extractPath; - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Extracting {package.Name} to {packageFolder}..."); + App.Logger.WriteLine(LOG_IDENT, $"Reading {package.Name}..."); + + var archive = await Task.Run(() => ZipFile.OpenRead(packageLocation)); - using ZipArchive archive = await Task.Run(() => ZipFile.OpenRead(packageLocation)); + App.Logger.WriteLine(LOG_IDENT, $"Read {package.Name}. Extracting to {packageFolder}..."); - foreach (ZipArchiveEntry entry in archive.Entries) + // yeah so because roblox is roblox, these packages aren't actually valid zip files + // besides the fact that they use backslashes instead of forward slashes for directories, + // empty folders that *BEGIN* with a backslash in their fullname, but have an empty name are listed here for some reason... + + foreach (var entry in archive.Entries) { if (_cancelFired) return; - if (entry.FullName.EndsWith('\\')) + if (String.IsNullOrEmpty(entry.Name)) continue; - extractPath = Path.Combine(packageFolder, entry.FullName); - - //App.Logger.WriteLine($"[{package.Name}] Writing {extractPath}..."); - + string extractPath = Path.Combine(packageFolder, entry.FullName); string? directory = Path.GetDirectoryName(extractPath); - if (directory is null) - continue; - - Directory.CreateDirectory(directory); + if (directory is not null) + Directory.CreateDirectory(directory); - using var fileStream = new FileStream(extractPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 0x1000, useAsync: true); - using var dataStream = entry.Open(); - - await dataStream.CopyToAsync(fileStream); - - File.SetLastWriteTime(extractPath, entry.LastWriteTime.DateTime); + entry.ExtractToFile(extractPath, true); } - App.Logger.WriteLine($"[Bootstrapper::ExtractPackage] Finished extracting {package.Name}"); + App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); _packagesExtracted += 1; } - private void ExtractFileFromPackage(string packageName, string fileName) + private async Task ExtractFileFromPackage(string packageName, string fileName) { Package? package = _versionPackageManifest.Find(x => x.Name == packageName); if (package is null) return; - DownloadPackage(package).GetAwaiter().GetResult(); - - string packageLocation = Path.Combine(Directories.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + await DownloadPackage(package); - using ZipArchive archive = ZipFile.OpenRead(packageLocation); + 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 fileLocation = Path.Combine(packageFolder, entry.FullName); - - File.Delete(fileLocation); - - entry.ExtractToFile(fileLocation); + string extractionPath = Path.Combine(_versionFolder, PackageDirectories[package.Name], entry.FullName); + entry.ExtractToFile(extractionPath, true); } #endregion } diff --git a/Bloxstrap/Exceptions/HttpResponseException.cs b/Bloxstrap/Exceptions/HttpResponseException.cs new file mode 100644 index 00000000..08404b07 --- /dev/null +++ b/Bloxstrap/Exceptions/HttpResponseException.cs @@ -0,0 +1,19 @@ +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/Exceptions/HttpResponseUnsuccessfulException.cs b/Bloxstrap/Exceptions/HttpResponseUnsuccessfulException.cs deleted file mode 100644 index 1409f788..00000000 --- a/Bloxstrap/Exceptions/HttpResponseUnsuccessfulException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Exceptions -{ - internal class HttpResponseUnsuccessfulException : Exception - { - public HttpResponseMessage ResponseMessage { get; } - - public HttpResponseUnsuccessfulException(HttpResponseMessage responseMessage) : base() - { - ResponseMessage = responseMessage; - } - } -} diff --git a/Bloxstrap/Extensions/BootstrapperIconEx.cs b/Bloxstrap/Extensions/BootstrapperIconEx.cs index 90656a76..49df9698 100644 --- a/Bloxstrap/Extensions/BootstrapperIconEx.cs +++ b/Bloxstrap/Extensions/BootstrapperIconEx.cs @@ -10,6 +10,8 @@ static class BootstrapperIconEx public static Icon GetIcon(this BootstrapperIcon icon) { + const string LOG_IDENT = "BootstrapperIconEx::GetIcon"; + // load the custom icon file if (icon == BootstrapperIcon.IconCustom) { @@ -21,7 +23,8 @@ public static Icon GetIcon(this BootstrapperIcon icon) } catch (Exception ex) { - App.Logger.WriteLine($"[BootstrapperIconEx::GetIcon] Failed to load custom icon! {ex}"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to load custom icon!"); + App.Logger.WriteException(LOG_IDENT, ex); } return customIcon ?? Properties.Resources.IconBloxstrap; diff --git a/Bloxstrap/Extensions/EmojiTypeEx.cs b/Bloxstrap/Extensions/EmojiTypeEx.cs index 0eb9d6ea..4313fabf 100644 --- a/Bloxstrap/Extensions/EmojiTypeEx.cs +++ b/Bloxstrap/Extensions/EmojiTypeEx.cs @@ -34,7 +34,7 @@ public static string GetUrl(this EmojiType emojiType) if (emojiType == EmojiType.Default) return ""; - return $"https://github.com/NikSavchenk0/rbxcustom-fontemojis/raw/8a552f4aaaecfa58d6bd9b0540e1ac16e81faadb/{Filenames[emojiType]}"; + return $"https://github.com/pizzaboxer/rbxcustom-fontemojis/releases/download/my-phone-is-78-percent/{Filenames[emojiType]}"; } } } diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 404528d4..0a3e36a8 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -1,11 +1,13 @@ -using System.Windows.Input; -using System.Windows.Media.Animation; +using System.Windows.Forms; + +using Windows.Win32; +using Windows.Win32.Graphics.Gdi; namespace Bloxstrap { public class FastFlagManager : JsonManager> { - public override string FileLocation => Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json"); + public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json"); // this is the value of the 'FStringPartTexturePackTablePre2022' flag public const string OldTexturesFlagValue = "{\"foil\":{\"ids\":[\"rbxassetid://7546645012\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"brick\":{\"ids\":[\"rbxassetid://7546650097\",\"rbxassetid://7546645118\"],\"color\":[204,201,200,232]},\"cobblestone\":{\"ids\":[\"rbxassetid://7546652947\",\"rbxassetid://7546645118\"],\"color\":[212,200,187,250]},\"concrete\":{\"ids\":[\"rbxassetid://7546653951\",\"rbxassetid://7546654144\"],\"color\":[208,208,208,255]},\"diamondplate\":{\"ids\":[\"rbxassetid://7547162198\",\"rbxassetid://7546645118\"],\"color\":[170,170,170,255]},\"fabric\":{\"ids\":[\"rbxassetid://7547101130\",\"rbxassetid://7546645118\"],\"color\":[105,104,102,244]},\"glass\":{\"ids\":[\"rbxassetid://7547304948\",\"rbxassetid://7546645118\"],\"color\":[254,254,254,7]},\"granite\":{\"ids\":[\"rbxassetid://7547164710\",\"rbxassetid://7546645118\"],\"color\":[113,113,113,255]},\"grass\":{\"ids\":[\"rbxassetid://7547169285\",\"rbxassetid://7546645118\"],\"color\":[165,165,159,255]},\"ice\":{\"ids\":[\"rbxassetid://7547171356\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"marble\":{\"ids\":[\"rbxassetid://7547177270\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"metal\":{\"ids\":[\"rbxassetid://7547288171\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"pebble\":{\"ids\":[\"rbxassetid://7547291361\",\"rbxassetid://7546645118\"],\"color\":[208,208,208,255]},\"corrodedmetal\":{\"ids\":[\"rbxassetid://7547184629\",\"rbxassetid://7546645118\"],\"color\":[159,119,95,200]},\"sand\":{\"ids\":[\"rbxassetid://7547295153\",\"rbxassetid://7546645118\"],\"color\":[220,220,220,255]},\"slate\":{\"ids\":[\"rbxassetid://7547298114\",\"rbxassetid://7547298323\"],\"color\":[193,193,193,255]},\"wood\":{\"ids\":[\"rbxassetid://7547303225\",\"rbxassetid://7547298786\"],\"color\":[227,227,227,255]},\"woodplanks\":{\"ids\":[\"rbxassetid://7547332968\",\"rbxassetid://7546645118\"],\"color\":[212,209,203,255]},\"asphalt\":{\"ids\":[\"rbxassetid://9873267379\",\"rbxassetid://9438410548\"],\"color\":[123,123,123,234]},\"basalt\":{\"ids\":[\"rbxassetid://9873270487\",\"rbxassetid://9438413638\"],\"color\":[154,154,153,238]},\"crackedlava\":{\"ids\":[\"rbxassetid://9438582231\",\"rbxassetid://9438453972\"],\"color\":[74,78,80,156]},\"glacier\":{\"ids\":[\"rbxassetid://9438851661\",\"rbxassetid://9438453972\"],\"color\":[226,229,229,243]},\"ground\":{\"ids\":[\"rbxassetid://9439044431\",\"rbxassetid://9438453972\"],\"color\":[114,114,112,240]},\"leafygrass\":{\"ids\":[\"rbxassetid://9873288083\",\"rbxassetid://9438453972\"],\"color\":[121,117,113,234]},\"limestone\":{\"ids\":[\"rbxassetid://9873289812\",\"rbxassetid://9438453972\"],\"color\":[235,234,230,250]},\"mud\":{\"ids\":[\"rbxassetid://9873319819\",\"rbxassetid://9438453972\"],\"color\":[130,130,130,252]},\"pavement\":{\"ids\":[\"rbxassetid://9873322398\",\"rbxassetid://9438453972\"],\"color\":[142,142,144,236]},\"rock\":{\"ids\":[\"rbxassetid://9873515198\",\"rbxassetid://9438453972\"],\"color\":[154,154,154,248]},\"salt\":{\"ids\":[\"rbxassetid://9439566986\",\"rbxassetid://9438453972\"],\"color\":[220,220,221,255]},\"sandstone\":{\"ids\":[\"rbxassetid://9873521380\",\"rbxassetid://9438453972\"],\"color\":[174,171,169,246]},\"snow\":{\"ids\":[\"rbxassetid://9439632387\",\"rbxassetid://9438453972\"],\"color\":[218,218,218,255]}}"; @@ -20,11 +22,10 @@ public class FastFlagManager : JsonManager> { "HTTP.Proxy.Address.3", "DFStringHttpCurlProxyHostAndPortForExternalUrl" }, { "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" }, - { "Rendering.Fullscreen", "FFlagHandleAltEnterFullscreenManually" }, + { "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" }, { "Rendering.TexturePack", "FStringPartTexturePackTable2022" }, - - { "Rendering.DPI.Disable", "DFFlagDisableDPIScale" }, - { "Rendering.DPI.Variable", "DFFlagVariableDPIScale2" }, + { "Rendering.DisableScaling", "DFFlagDisableDPIScale" }, + { "Rendering.MSAA", "FIntDebugForceMSAASamples" }, { "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" }, { "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" }, @@ -63,6 +64,15 @@ public class FastFlagManager : JsonManager> { "Future (Phase 3)", "Future" } }; + public static IReadOnlyDictionary MSAAModes => new Dictionary + { + { "Automatic", null }, + { "1x MSAA", "1" }, + { "2x MSAA", "2" }, + { "4x MSAA", "4" }, + { "8x MSAA", "8" } + }; + // this is one hell of a dictionary definition lmao // since these all set the same flags, wouldn't making this use bitwise operators be better? public static IReadOnlyDictionary> IGMenuVersions => new Dictionary> @@ -108,19 +118,21 @@ public class FastFlagManager : JsonManager> // to delete a flag, set the value as null public void SetValue(string key, object? value) { + const string LOG_IDENT = "FastFlagManager::SetValue"; + if (value is null) { if (Prop.ContainsKey(key)) - App.Logger.WriteLine($"[FastFlagManager::SetValue] Deletion of '{key}' is pending"); + App.Logger.WriteLine(LOG_IDENT, $"Deletion of '{key}' is pending"); Prop.Remove(key); } else { if (Prop.ContainsKey(key)) - App.Logger.WriteLine($"[FastFlagManager::SetValue] Setting of '{key}' from '{Prop[key]}' to '{value}' is pending"); + App.Logger.WriteLine(LOG_IDENT, $"Changing of '{key}' from '{Prop[key]}' to '{value}' is pending"); else - App.Logger.WriteLine($"[FastFlagManager::SetValue] Setting of '{key}' to '{value}' is pending"); + App.Logger.WriteLine(LOG_IDENT, $"Setting of '{key}' to '{value}' is pending"); Prop[key] = value.ToString()!; } @@ -142,12 +154,6 @@ public void SetPreset(string prefix, object? value) SetValue(pair.Value, value); } - public void SetPresetOnce(string key, object? value) - { - if (GetPreset(key) is null) - SetPreset(key, value); - } - public void SetPresetEnum(string prefix, string target, object? value) { foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix))) @@ -175,6 +181,14 @@ public string GetPresetEnum(IReadOnlyDictionary mapping, string return mapping.First().Key; } + public void CheckManualFullscreenPreset() + { + if (GetPreset("Rendering.Mode.Vulkan") == "True" || GetPreset("Rendering.Mode.OpenGL") == "True") + SetPreset("Rendering.ManualFullscreen", null); + else + SetPreset("Rendering.ManualFullscreen", "False"); + } + public override void Save() { // convert all flag values to strings before saving @@ -189,11 +203,24 @@ public override void Load() { base.Load(); - SetPresetOnce("Rendering.Framerate", 9999); - SetPresetOnce("Rendering.Fullscreen", "False"); + CheckManualFullscreenPreset(); + + if (GetPreset("Rendering.Framerate") is not null) + return; + + // set it to be the framerate of the primary display by default + + var screen = Screen.AllScreens.Where(x => x.Primary).Single(); + var devmode = new DEVMODEW(); + + PInvoke.EnumDisplaySettings(screen.DeviceName, ENUM_DISPLAY_SETTINGS_MODE.ENUM_CURRENT_SETTINGS, ref devmode); + + uint framerate = devmode.dmDisplayFrequency; + + if (framerate <= 100) + framerate *= 2; - SetPresetOnce("Rendering.DPI.Disable", "True"); - SetPresetOnce("Rendering.DPI.Variable", "False"); + SetPreset("Rendering.Framerate", framerate); } } } diff --git a/Bloxstrap/GlobalUsings.cs b/Bloxstrap/GlobalUsings.cs index bcb19310..105c8d91 100644 --- a/Bloxstrap/GlobalUsings.cs +++ b/Bloxstrap/GlobalUsings.cs @@ -15,8 +15,10 @@ global using System.Threading.Tasks; global using Bloxstrap.Enums; +global using Bloxstrap.Exceptions; global using Bloxstrap.Extensions; global using Bloxstrap.Models; +global using Bloxstrap.Models.BloxstrapRPC; global using Bloxstrap.Models.Attributes; global using Bloxstrap.Models.RobloxApi; global using Bloxstrap.UI; diff --git a/Bloxstrap/HttpClientLoggingHandler.cs b/Bloxstrap/HttpClientLoggingHandler.cs index a39b94ad..3eb830b3 100644 --- a/Bloxstrap/HttpClientLoggingHandler.cs +++ b/Bloxstrap/HttpClientLoggingHandler.cs @@ -9,13 +9,13 @@ public HttpClientLoggingHandler(HttpMessageHandler innerHandler) protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) { - App.Logger.WriteLine($"[HttpClientLoggingHandler::HttpRequestMessage] {request.Method} {request.RequestUri}"); + App.Logger.WriteLine("HttpClientLoggingHandler::ProcessRequest", $"{request.Method} {request.RequestUri}"); return request; } protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken) { - App.Logger.WriteLine($"[HttpClientLoggingHandler::HttpResponseMessage] {(int)response.StatusCode} {response.ReasonPhrase} {response.RequestMessage!.RequestUri}"); + App.Logger.WriteLine("HttpClientLoggingHandler::ProcessResponse", $"{(int)response.StatusCode} {response.ReasonPhrase} {response.RequestMessage!.RequestUri}"); return response; } } diff --git a/Bloxstrap/InstallChecker.cs b/Bloxstrap/InstallChecker.cs new file mode 100644 index 00000000..285d61dd --- /dev/null +++ b/Bloxstrap/InstallChecker.cs @@ -0,0 +1,232 @@ +using System.Windows; + +using Microsoft.Win32; + +namespace Bloxstrap +{ + internal class InstallChecker : IDisposable + { + private RegistryKey? _registryKey; + private string? _installLocation; + + internal InstallChecker() + { + _registryKey = Registry.CurrentUser.OpenSubKey($"Software\\{App.ProjectName}", true); + + if (_registryKey is not null) + _installLocation = (string?)_registryKey.GetValue("InstallLocation"); + } + + internal void Check() + { + const string LOG_IDENT = "InstallChecker::Check"; + + if (_registryKey is null || _installLocation is null) + { + if (!File.Exists("Settings.json") || !File.Exists("State.json")) + { + FirstTimeRun(); + return; + } + + _installLocation = Path.GetDirectoryName(Paths.Process)!; + + App.Logger.WriteLine(LOG_IDENT, $"Registry key is likely malformed. Setting install location as '{_installLocation}'"); + + if (_registryKey is null) + _registryKey = Registry.CurrentUser.CreateSubKey($"Software\\{App.ProjectName}"); + + _registryKey.SetValue("InstallLocation", _installLocation); + } + + // check if drive that bloxstrap was installed to was removed from system, or had its drive letter changed + + if (!Directory.Exists(_installLocation)) + { + App.Logger.WriteLine(LOG_IDENT, "Could not find install location. Checking if drive has changed..."); + + bool driveExists = false; + string driveName = _installLocation[..3]; + string? newDriveName = null; + + foreach (var drive in DriveInfo.GetDrives()) + { + if (drive.Name == driveName) + driveExists = true; + else if (Directory.Exists(_installLocation.Replace(driveName, drive.Name))) + newDriveName = drive.Name; + } + + if (newDriveName is not null) + { + App.Logger.WriteLine(LOG_IDENT, $"Drive has changed from {driveName} to {newDriveName}"); + + Controls.ShowMessageBox( + $"{App.ProjectName} has detected a drive letter change and has reconfigured its install location from the {driveName} drive to the {newDriveName} drive.\n" + + "\n" + + $"While {App.ProjectName} will continue to work, it's recommended that you change the drive leter back to its original value as other installed applications can experience similar issues.", + MessageBoxImage.Warning, + MessageBoxButton.OK + ); + + _installLocation = _installLocation.Replace(driveName, newDriveName); + _registryKey.SetValue("InstallLocation", _installLocation); + } + else if (!driveExists) + { + App.Logger.WriteLine(LOG_IDENT, $"Drive {driveName} does not exist anymore, and has likely been removed"); + + var result = Controls.ShowMessageBox( + $"{App.ProjectName} was originally installed to the {driveName} drive, but it appears to no longer be present. Would you like to continue and carry out a fresh install?", + MessageBoxImage.Warning, + MessageBoxButton.OKCancel + ); + + if (result != MessageBoxResult.OK) + App.Terminate(); + + FirstTimeRun(); + return; + } + else + { + App.Logger.WriteLine(LOG_IDENT, "Drive has not changed, folder was likely moved or deleted"); + } + } + + App.BaseDirectory = _installLocation; + App.IsFirstRun = false; + } + + public void Dispose() + { + _registryKey?.Dispose(); + GC.SuppressFinalize(this); + } + + private static void FirstTimeRun() + { + const string LOG_IDENT = "InstallChecker::FirstTimeRun"; + + App.Logger.WriteLine(LOG_IDENT, "Running first-time install"); + + App.BaseDirectory = Path.Combine(Paths.LocalAppData, App.ProjectName); + App.Logger.Initialize(true); + + if (App.IsQuiet) + return; + + App.IsSetupComplete = false; + + App.FastFlags.Load(); + Controls.ShowMenu(); + + // exit if we don't click the install button on installation + if (App.IsSetupComplete) + return; + + App.Logger.WriteLine(LOG_IDENT, "Installation cancelled!"); + App.Terminate(ErrorCode.ERROR_CANCELLED); + } + + internal static void CheckUpgrade() + { + const string LOG_IDENT = "InstallChecker::CheckUpgrade"; + + if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application) + return; + + // 2.0.0 downloads updates to /Updates so lol + bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")); + + FileVersionInfo existingVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Application); + FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Process); + + if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application)) + return; + + MessageBoxResult result; + + // silently upgrade version if the command line flag is set or if we're launching from an auto update + if (App.IsUpgrade || isAutoUpgrade) + { + result = MessageBoxResult.Yes; + } + else + { + result = Controls.ShowMessageBox( + $"The version of {App.ProjectName} you've launched is different to the version you currently have installed.\nWould you like to upgrade your currently installed version?", + MessageBoxImage.Question, + MessageBoxButton.YesNo + ); + } + + if (result != MessageBoxResult.Yes) + return; + + Filesystem.AssertReadOnly(Paths.Application); + + // yes, this is EXTREMELY hacky, but the updater process that launched the + // new version may still be open and so we have to wait for it to close + int attempts = 0; + while (attempts < 10) + { + attempts++; + + try + { + File.Delete(Paths.Application); + break; + } + catch (Exception) + { + if (attempts == 1) + App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); + + Thread.Sleep(500); + } + } + + if (attempts == 10) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)"); + return; + } + + File.Copy(Paths.Process, Paths.Application); + + Bootstrapper.Register(); + + // update migrations + + if (App.BuildMetadata.CommitRef.StartsWith("tag") && existingVersionInfo.ProductVersion == "2.4.0") + { + App.FastFlags.SetValue("DFFlagDisableDPIScale", null); + App.FastFlags.SetValue("DFFlagVariableDPIScale2", null); + App.FastFlags.Save(); + } + + if (isAutoUpgrade) + { + App.NotifyIcon?.ShowAlert( + $"{App.ProjectName} has been upgraded to v{currentVersionInfo.ProductVersion}", + "See what's new in this version", + 30, + (_, _) => Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/releases/tag/v{currentVersionInfo.ProductVersion}") + ); + } + else if (!App.IsQuiet) + { + Controls.ShowMessageBox( + $"{App.ProjectName} has been upgraded to v{currentVersionInfo.ProductVersion}", + MessageBoxImage.Information, + MessageBoxButton.OK + ); + + Controls.ShowMenu(); + + App.Terminate(); + } + } + } +} diff --git a/Bloxstrap/RobloxActivity.cs b/Bloxstrap/Integrations/ActivityWatcher.cs similarity index 69% rename from Bloxstrap/RobloxActivity.cs rename to Bloxstrap/Integrations/ActivityWatcher.cs index 0c5ab82a..0571e3c3 100644 --- a/Bloxstrap/RobloxActivity.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -1,6 +1,6 @@ -namespace Bloxstrap +namespace Bloxstrap.Integrations { - public class RobloxActivity : IDisposable + public class ActivityWatcher : IDisposable { // i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence, // like checking the ping and region of the current connected server. maybe that's something to add? @@ -11,7 +11,7 @@ public class RobloxActivity : IDisposable private const string GameJoinedEntry = "[FLog::Network] serverId:"; private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:"; private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport"; - private const string GameMessageEntry = "[FLog::Output] [SendBloxstrapMessage]"; + private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]"; private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+"; @@ -24,9 +24,10 @@ public class RobloxActivity : IDisposable public event EventHandler? OnLogEntry; public event EventHandler? OnGameJoin; public event EventHandler? OnGameLeave; - public event EventHandler? OnGameMessage; + public event EventHandler? OnRPCMessage; - private Dictionary GeolcationCache = new(); + private readonly Dictionary GeolocationCache = new(); + private DateTime LastRPCRequest; public string LogLocation = null!; @@ -44,6 +45,8 @@ public class RobloxActivity : IDisposable public async void StartWatcher() { + const string LOG_IDENT = "ActivityWatcher::StartWatcher"; + // okay, here's the process: // // - tail the latest log file from %localappdata%\roblox\logs @@ -60,7 +63,7 @@ public async void StartWatcher() if (App.Settings.Prop.OhHeyYouFoundMe) delay = 250; - string logDirectory = Path.Combine(Directories.LocalAppData, "Roblox\\logs"); + string logDirectory = Path.Combine(Paths.LocalAppData, "Roblox\\logs"); if (!Directory.Exists(logDirectory)) return; @@ -71,7 +74,7 @@ public async void StartWatcher() // if roblox doesn't start quickly enough, we can wind up fetching the previous log file // good rule of thumb is to find a log file that was created in the last 15 seconds or so - App.Logger.WriteLine("[RobloxActivity::StartWatcher] Opening Roblox log file..."); + App.Logger.WriteLine(LOG_IDENT, "Opening Roblox log file..."); while (true) { @@ -84,13 +87,13 @@ public async void StartWatcher() if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now) break; - App.Logger.WriteLine($"[RobloxActivity::StartWatcher] Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})"); + App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})"); await Task.Delay(1000); } LogLocation = logFileInfo.FullName; FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - App.Logger.WriteLine($"[RobloxActivity::StartWatcher] Opened {LogLocation}"); + App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}"); AutoResetEvent logUpdatedEvent = new(false); FileSystemWatcher logWatcher = new() @@ -116,6 +119,8 @@ public async void StartWatcher() private void ExamineLogEntry(string entry) { + const string LOG_IDENT = "ActivityWatcher::ExamineLogEntry"; + OnLogEntry?.Invoke(this, entry); _logEntriesRead += 1; @@ -123,9 +128,9 @@ private void ExamineLogEntry(string entry) // debug stats to ensure that the log reader is working correctly // if more than 1000 log entries have been read, only log per 100 to save on spam if (_logEntriesRead <= 1000 && _logEntriesRead % 50 == 0) - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Read {_logEntriesRead} log entries"); + App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries"); else if (_logEntriesRead % 100 == 0) - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Read {_logEntriesRead} log entries"); + App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries"); if (!ActivityInGame && ActivityPlaceId == 0) { @@ -140,8 +145,8 @@ private void ExamineLogEntry(string entry) if (match.Groups.Count != 4) { - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game join entry"); - App.Logger.WriteLine(entry); + App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join entry"); + App.Logger.WriteLine(LOG_IDENT, entry); return; } @@ -162,7 +167,7 @@ private void ExamineLogEntry(string entry) _reservedTeleportMarker = false; } - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); } } else if (!ActivityInGame && ActivityPlaceId != 0) @@ -173,15 +178,15 @@ private void ExamineLogEntry(string entry) if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress) { - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game join UDMUX entry"); - App.Logger.WriteLine(entry); + App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join UDMUX entry"); + App.Logger.WriteLine(LOG_IDENT, entry); return; } ActivityMachineAddress = match.Groups[1].Value; ActivityMachineUDMUX = true; - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); } else if (entry.Contains(GameJoinedEntry)) { @@ -189,12 +194,12 @@ private void ExamineLogEntry(string entry) if (match.Groups.Count != 2 || match.Groups[1].Value != ActivityMachineAddress) { - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game joined entry"); - App.Logger.WriteLine(entry); + App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry"); + App.Logger.WriteLine(LOG_IDENT, entry); return; } - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); ActivityInGame = true; OnGameJoin?.Invoke(this, new EventArgs()); @@ -204,7 +209,7 @@ private void ExamineLogEntry(string entry) { if (entry.Contains(GameDisconnectedEntry)) { - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); ActivityInGame = false; ActivityPlaceId = 0; @@ -218,7 +223,7 @@ private void ExamineLogEntry(string entry) } else if (entry.Contains(GameTeleportingEntry)) { - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); _teleportMarker = true; } else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry)) @@ -229,60 +234,83 @@ private void ExamineLogEntry(string entry) else if (entry.Contains(GameMessageEntry)) { string messagePlain = entry.Substring(entry.IndexOf(GameMessageEntry) + GameMessageEntry.Length + 1); - GameMessage? message; + Message? message; - App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Received message: '{messagePlain}'"); + App.Logger.WriteLine(LOG_IDENT, $"Received message: '{messagePlain}'"); + + if ((DateTime.Now - LastRPCRequest).TotalSeconds <= 1) + { + App.Logger.WriteLine(LOG_IDENT, "Dropping message as ratelimit has been hit"); + return; + } try { - message = JsonSerializer.Deserialize(messagePlain); + message = JsonSerializer.Deserialize(messagePlain); } catch (Exception) { - App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (JSON deserialization threw an exception)"); + App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)"); return; } if (message is null) { - App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (JSON deserialization returned null)"); + App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)"); return; } - if (String.IsNullOrEmpty(message.Command)) + if (string.IsNullOrEmpty(message.Command)) { - App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (Command is empty)"); + App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (Command is empty)"); return; } - OnGameMessage?.Invoke(this, message); + OnRPCMessage?.Invoke(this, message); + + LastRPCRequest = DateTime.Now; } } } public async Task GetServerLocation() { - if (GeolcationCache.ContainsKey(ActivityMachineAddress)) - return GeolcationCache[ActivityMachineAddress]; + const string LOG_IDENT = "ActivityWatcher::GetServerLocation"; - string location = ""; + if (GeolocationCache.ContainsKey(ActivityMachineAddress)) + return GeolocationCache[ActivityMachineAddress]; - string locationCity = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/city"); - string locationRegion = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/region"); - string locationCountry = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/country"); + string location, locationCity, locationRegion, locationCountry = ""; + + try + { + locationCity = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/city"); + locationRegion = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/region"); + locationCountry = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/country"); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {ActivityMachineAddress}"); + App.Logger.WriteException(LOG_IDENT, ex); + + return "N/A (lookup failed)"; + } locationCity = locationCity.ReplaceLineEndings(""); locationRegion = locationRegion.ReplaceLineEndings(""); locationCountry = locationCountry.ReplaceLineEndings(""); - if (String.IsNullOrEmpty(locationCountry)) + if (string.IsNullOrEmpty(locationCountry)) location = "N/A"; else if (locationCity == locationRegion) location = $"{locationRegion}, {locationCountry}"; else location = $"{locationCity}, {locationRegion}, {locationCountry}"; - GeolcationCache[ActivityMachineAddress] = location; + if (!ActivityInGame) + return "N/A (left game)"; + + GeolocationCache[ActivityMachineAddress] = location; return location; } diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs index 5e2a18ca..1444b369 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -5,94 +5,158 @@ namespace Bloxstrap.Integrations public class DiscordRichPresence : IDisposable { private readonly DiscordRpcClient _rpcClient = new("1005469189907173486"); - private readonly RobloxActivity _activityWatcher; + private readonly ActivityWatcher _activityWatcher; - private RichPresence? _currentPresence; + private DiscordRPC.RichPresence? _currentPresence; + private DiscordRPC.RichPresence? _currentPresenceCopy; + private bool _visible = true; - private string? _initialStatus; private long _currentUniverseId; private DateTime? _timeStartedUniverse; - public DiscordRichPresence(RobloxActivity activityWatcher) + public DiscordRichPresence(ActivityWatcher activityWatcher) { + const string LOG_IDENT = "DiscordRichPresence::DiscordRichPresence"; + _activityWatcher = activityWatcher; _activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame()); _activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame()); - _activityWatcher.OnGameMessage += (_, message) => OnGameMessage(message); + _activityWatcher.OnRPCMessage += (_, message) => ProcessRPCMessage(message); _rpcClient.OnReady += (_, e) => - App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})"); + App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})"); _rpcClient.OnPresenceUpdate += (_, e) => - App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Presence updated"); + App.Logger.WriteLine(LOG_IDENT, "Presence updated"); _rpcClient.OnError += (_, e) => - App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] An RPC error occurred - {e.Message}"); + App.Logger.WriteLine(LOG_IDENT, $"An RPC error occurred - {e.Message}"); _rpcClient.OnConnectionEstablished += (_, e) => - App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Established connection with Discord RPC"); + App.Logger.WriteLine(LOG_IDENT, "Established connection with Discord RPC"); //spams log as it tries to connect every ~15 sec when discord is closed so not now //_rpcClient.OnConnectionFailed += (_, e) => - // App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Failed to establish connection with Discord RPC"); + // App.Logger.WriteLine(LOG_IDENT, "Failed to establish connection with Discord RPC"); _rpcClient.OnClose += (_, e) => - App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Lost connection to Discord RPC - {e.Reason} ({e.Code})"); + App.Logger.WriteLine(LOG_IDENT, $"Lost connection to Discord RPC - {e.Reason} ({e.Code})"); _rpcClient.Initialize(); } - public void OnGameMessage(GameMessage message) + public void ProcessRPCMessage(Message message) { - if (message.Command == "SetPresenceStatus") - SetStatus(message.Data); - } + const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage"; - public void SetStatus(string status) - { - App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Setting status to '{status}'"); + if (message.Command != "SetRichPresence") + return; - if (_currentPresence is null) + if (_currentPresence is null || _currentPresenceCopy is null) { - App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Presence is not set, aborting"); + App.Logger.WriteLine(LOG_IDENT, "Presence is not set, aborting"); return; } - if (status.Length > 128) + Models.BloxstrapRPC.RichPresence? presenceData; + + // a lot of repeated code here, could this somehow be cleaned up? + + try + { + presenceData = message.Data.Deserialize(); + } + catch (Exception) { - App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status cannot be longer than 128 characters, aborting"); + App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)"); return; } - if (_initialStatus is null) - _initialStatus = _currentPresence.State; + if (presenceData is null) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)"); + return; + } - string finalStatus; + if (presenceData.Details is not null) + { + if (presenceData.Details.Length > 128) + App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters"); + else if (presenceData.Details == "") + _currentPresence.Details = _currentPresenceCopy.Details; + else + _currentPresence.Details = presenceData.Details; + } - if (string.IsNullOrEmpty(status)) + if (presenceData.State is not null) { - App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status is empty, reverting to initial status"); - finalStatus = _initialStatus; + if (presenceData.State.Length > 128) + App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters"); + else if (presenceData.State == "") + _currentPresence.State = _currentPresenceCopy.State; + else + _currentPresence.State = presenceData.State; } - else + + if (presenceData.TimestampStart == 0) + _currentPresence.Timestamps.Start = null; + else if (presenceData.TimestampStart is not null) + _currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000; + + if (presenceData.TimestampEnd == 0) + _currentPresence.Timestamps.End = null; + else if (presenceData.TimestampEnd is not null) + _currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000; + + if (presenceData.SmallImage is not null) { - finalStatus = status; + if (presenceData.SmallImage.Clear) + { + _currentPresence.Assets.SmallImageKey = ""; + } + else if (presenceData.SmallImage.Reset) + { + _currentPresence.Assets.SmallImageText = _currentPresenceCopy.Assets.SmallImageText; + _currentPresence.Assets.SmallImageKey = _currentPresenceCopy.Assets.SmallImageKey; + } + else + { + if (presenceData.SmallImage.AssetId is not null) + _currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}"; + + if (presenceData.SmallImage.HoverText is not null) + _currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText; + } } - if (_currentPresence.State == finalStatus) + if (presenceData.LargeImage is not null) { - App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status is unchanged, aborting"); - return; + if (presenceData.LargeImage.Clear) + { + _currentPresence.Assets.LargeImageKey = ""; + } + else if (presenceData.LargeImage.Reset) + { + _currentPresence.Assets.LargeImageText = _currentPresenceCopy.Assets.LargeImageText; + _currentPresence.Assets.LargeImageKey = _currentPresenceCopy.Assets.LargeImageKey; + } + else + { + if (presenceData.LargeImage.AssetId is not null) + _currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}"; + + if (presenceData.LargeImage.HoverText is not null) + _currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText; + } } - _currentPresence.State = finalStatus; UpdatePresence(); } public void SetVisibility(bool visible) { - App.Logger.WriteLine($"[DiscordRichPresence::SetVisibility] Setting presence visibility ({visible})"); + App.Logger.WriteLine("DiscordRichPresence::SetVisibility", $"Setting presence visibility ({visible})"); _visible = visible; @@ -104,11 +168,12 @@ public void SetVisibility(bool visible) public async Task SetCurrentGame() { + const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame"; + if (!_activityWatcher.ActivityInGame) { - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Not in game, clearing presence"); - _currentPresence = null; - _initialStatus = null; + App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence"); + _currentPresence = _currentPresenceCopy = null; UpdatePresence(); return true; } @@ -116,17 +181,17 @@ public async Task SetCurrentGame() string icon = "roblox"; long placeId = _activityWatcher.ActivityPlaceId; - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Setting presence for Place ID {placeId}"); + App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}"); var universeIdResponse = await Http.GetJson($"https://apis.roblox.com/universes/v1/places/{placeId}/universe"); if (universeIdResponse is null) { - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe ID!"); + App.Logger.WriteLine(LOG_IDENT, "Could not get Universe ID!"); return false; } long universeId = universeIdResponse.UniverseId; - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe ID as {universeId}"); + App.Logger.WriteLine(LOG_IDENT, $"Got Universe ID as {universeId}"); // preserve time spent playing if we're teleporting between places in the same universe if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId) @@ -137,22 +202,22 @@ public async Task SetCurrentGame() var gameDetailResponse = await Http.GetJson>($"https://games.roblox.com/v1/games?universeIds={universeId}"); if (gameDetailResponse is null || !gameDetailResponse.Data.Any()) { - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe info!"); + App.Logger.WriteLine(LOG_IDENT, "Could not get Universe info!"); return false; } GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0]; - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe details"); + App.Logger.WriteLine(LOG_IDENT, "Got Universe details"); var universeThumbnailResponse = await Http.GetJson>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any()) { - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe thumbnail info!"); + App.Logger.WriteLine(LOG_IDENT, "Could not get Universe thumbnail info!"); } else { icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl; - App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe thumbnail as {icon}"); + App.Logger.WriteLine(LOG_IDENT, $"Got Universe thumbnail as {icon}"); } List