diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index f5bcefb..bbc3b65 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -1,312 +1,320 @@ -using System.Reflection; -using System.Security.Cryptography; -using System.Windows; -using System.Windows.Shell; -using System.Windows.Threading; - -using Microsoft.Win32; - -namespace Bloxstrap -{ - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { -#if QA_BUILD - public const string ProjectName = "Bloxstrap-QA"; -#else - public const string ProjectName = "Bloxstrap"; -#endif - public const string ProjectOwner = "Bloxstrap"; - public const string ProjectRepository = "bloxstraplabs/bloxstrap"; - public const string ProjectDownloadLink = "https://bloxstraplabs.com"; - public const string ProjectHelpLink = "https://github.com/bloxstraplabs/bloxstrap/wiki"; - public const string ProjectSupportLink = "https://github.com/bloxstraplabs/bloxstrap/issues/new"; - - public const string RobloxPlayerAppName = "RobloxPlayerBeta"; - public const string RobloxStudioAppName = "RobloxStudioBeta"; - - // simple shorthand for extremely frequently used and long string - this goes under HKCU - public const string UninstallKey = $@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}"; - - public static LaunchSettings LaunchSettings { get; private set; } = null!; - - public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute()!; - - public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; - - public static Bootstrapper? Bootstrapper { get; set; } = null!; - - public static bool IsActionBuild => !String.IsNullOrEmpty(BuildMetadata.CommitRef); - - public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal); - - public static readonly MD5 MD5Provider = MD5.Create(); - - public static readonly Logger Logger = new(); - - public static readonly Dictionary PendingSettingTasks = new(); - - public static readonly JsonManager Settings = new(); - - public static readonly JsonManager State = new(); - - public static readonly FastFlagManager FastFlags = new(); - - public static readonly HttpClient HttpClient = new( - new HttpClientLoggingHandler( - new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All } - ) - ); - - private static bool _showingExceptionDialog = false; - - public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) - { - int exitCodeNum = (int)exitCode; - - Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); - - Environment.Exit(exitCodeNum); - } - - public static void SoftTerminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) - { - int exitCodeNum = (int)exitCode; - - Logger.WriteLine("App::SoftTerminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); - - Current.Dispatcher.Invoke(() => Current.Shutdown(exitCodeNum)); - } - - void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e) - { - e.Handled = true; - - Logger.WriteLine("App::GlobalExceptionHandler", "An exception occurred"); - - FinalizeExceptionHandling(e.Exception); - } - - public static void FinalizeExceptionHandling(AggregateException ex) - { - foreach (var innerEx in ex.InnerExceptions) - Logger.WriteException("App::FinalizeExceptionHandling", innerEx); - - FinalizeExceptionHandling(ex.GetBaseException(), false); - } - - public static void FinalizeExceptionHandling(Exception ex, bool log = true) - { - if (log) - Logger.WriteException("App::FinalizeExceptionHandling", ex); - - if (_showingExceptionDialog) - return; - - _showingExceptionDialog = true; - - if (Bootstrapper?.Dialog != null) - { - if (Bootstrapper.Dialog.TaskbarProgressValue == 0) - Bootstrapper.Dialog.TaskbarProgressValue = 1; // make sure it's visible - - Bootstrapper.Dialog.TaskbarProgressState = TaskbarItemProgressState.Error; - } - - Frontend.ShowExceptionDialog(ex); - - Terminate(ErrorCode.ERROR_INSTALL_FAILURE); - } - - public static async Task GetLatestRelease() - { - const string LOG_IDENT = "App::GetLatestRelease"; - - try - { - var releaseInfo = await Http.GetJson($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); - - if (releaseInfo is null || releaseInfo.Assets is null) - { - Logger.WriteLine(LOG_IDENT, "Encountered invalid data"); - return null; - } - - return releaseInfo; - } - catch (Exception ex) - { - Logger.WriteException(LOG_IDENT, ex); - } - - return null; - } - - public static async void SendStat(string key, string value) - { - if (!Settings.Prop.EnableAnalytics) - return; - - try - { - await HttpClient.GetAsync($"https://bloxstraplabs.com/metrics/post?key={key}&value={value}"); - } - catch (Exception ex) - { - Logger.WriteException("App::SendStat", ex); - } - } - - protected override void OnStartup(StartupEventArgs e) - { - const string LOG_IDENT = "App::OnStartup"; - - Locale.Initialize(); - - base.OnStartup(e); - - Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}"); - - string userAgent = $"{ProjectName}/{Version}"; - - if (IsActionBuild) - { - Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})"); - - if (IsProductionBuild) - userAgent += $" (Production)"; - else - userAgent += $" (Artifact {BuildMetadata.CommitHash}, {BuildMetadata.CommitRef})"; - } - else - { - Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}"); - -#if QA_BUILD - userAgent += " (QA)"; -#else - userAgent += $" (Build {Convert.ToBase64String(Encoding.UTF8.GetBytes(BuildMetadata.Machine))})"; -#endif - } - - Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}"); - Logger.WriteLine(LOG_IDENT, $"Temp path is {Paths.Temp}"); - Logger.WriteLine(LOG_IDENT, $"WindowsStartMenu path is {Paths.WindowsStartMenu}"); - - // To customize application configuration such as set high DPI settings or default font, - // see https://aka.ms/applicationconfiguration. - ApplicationConfiguration.Initialize(); - - HttpClient.Timeout = TimeSpan.FromSeconds(30); - HttpClient.DefaultRequestHeaders.Add("User-Agent", userAgent); - - LaunchSettings = new LaunchSettings(e.Args); - - // installation check begins here - using var uninstallKey = Registry.CurrentUser.OpenSubKey(UninstallKey); - string? installLocation = null; - bool fixInstallLocation = false; - - if (uninstallKey?.GetValue("InstallLocation") is string value) - { - if (Directory.Exists(value)) - { - installLocation = value; - } - else - { - // check if user profile folder has been renamed - // honestly, i'll be expecting bugs from this - var match = Regex.Match(value, @"^[a-zA-Z]:\\Users\\([^\\]+)", RegexOptions.IgnoreCase); - - if (match.Success) - { - string newLocation = value.Replace(match.Value, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase); - - if (Directory.Exists(newLocation)) - { - installLocation = newLocation; - fixInstallLocation = true; - } - } - } - } - - // silently change install location if we detect a portable run - if (installLocation is null && Directory.GetParent(Paths.Process)?.FullName is string processDir) - { - var files = Directory.GetFiles(processDir).Select(x => Path.GetFileName(x)).ToArray(); - - // check if settings.json and state.json are the only files in the folder - if (files.Length <= 3 && files.Contains("Settings.json") && files.Contains("State.json")) - { - installLocation = processDir; - fixInstallLocation = true; - } - } - - if (fixInstallLocation && installLocation is not null) - { - var installer = new Installer - { - InstallLocation = installLocation, - IsImplicitInstall = true - }; - - if (installer.CheckInstallLocation()) - { - Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'"); - installer.DoInstall(); - } - else - { - // force reinstall - installLocation = null; - } - } - - if (installLocation is null) - { - Logger.Initialize(true); - LaunchHandler.LaunchInstaller(); - } - else - { - Paths.Initialize(installLocation); - - // ensure executable is in the install directory - if (Paths.Process != Paths.Application && !File.Exists(Paths.Application)) - File.Copy(Paths.Process, Paths.Application); - - Logger.Initialize(LaunchSettings.UninstallFlag.Active); - - if (!Logger.Initialized && !Logger.NoWriteMode) - { - Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating."); - Terminate(); - } - - Settings.Load(); - State.Load(); - FastFlags.Load(); - - if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale)) - { - Settings.Prop.Locale = "nil"; - Settings.Save(); - } - - Locale.Set(Settings.Prop.Locale); - - if (!LaunchSettings.BypassUpdateCheck) - Installer.HandleUpgrade(); - - LaunchHandler.ProcessLaunchArgs(); - } - - // you must *explicitly* call terminate when everything is done, it won't be called implicitly - } - } -} +using System.Reflection; +using System.Security.Cryptography; +using System.Windows; +using System.Windows.Shell; +using System.Windows.Threading; + +using Microsoft.Win32; + +namespace Bloxstrap +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { +#if QA_BUILD + public const string ProjectName = "Bloxstrap-QA"; +#else + public const string ProjectName = "Bloxstrap"; +#endif + public const string ProjectOwner = "Bloxstrap"; + public const string ProjectRepository = "bloxstraplabs/bloxstrap"; + public const string ProjectDownloadLink = "https://bloxstraplabs.com"; + public const string ProjectHelpLink = "https://github.com/bloxstraplabs/bloxstrap/wiki"; + public const string ProjectSupportLink = "https://github.com/bloxstraplabs/bloxstrap/issues/new"; + + public const string RobloxPlayerAppName = "RobloxPlayerBeta"; + public const string RobloxStudioAppName = "RobloxStudioBeta"; + + // simple shorthand for extremely frequently used and long string - this goes under HKCU + public const string UninstallKey = $@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}"; + + public static LaunchSettings LaunchSettings { get; private set; } = null!; + + public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute()!; + + public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; + + public static Bootstrapper? Bootstrapper { get; set; } = null!; + + public static bool IsActionBuild => !String.IsNullOrEmpty(BuildMetadata.CommitRef); + + public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal); + + public static readonly MD5 MD5Provider = MD5.Create(); + + public static readonly Logger Logger = new(); + + public static readonly Dictionary PendingSettingTasks = new(); + + public static readonly JsonManager Settings = new(); + + public static readonly JsonManager State = new(); + + public static readonly FastFlagManager FastFlags = new(); + + public static readonly HttpClient HttpClient = new( + new HttpClientLoggingHandler( + new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All } + ) + ); + + private static bool _showingExceptionDialog = false; + + public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) + { + int exitCodeNum = (int)exitCode; + + Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); + + Environment.Exit(exitCodeNum); + } + + public static void SoftTerminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) + { + int exitCodeNum = (int)exitCode; + + Logger.WriteLine("App::SoftTerminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); + + Current.Dispatcher.Invoke(() => Current.Shutdown(exitCodeNum)); + } + + void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + + Logger.WriteLine("App::GlobalExceptionHandler", "An exception occurred"); + + FinalizeExceptionHandling(e.Exception); + } + + public static void FinalizeExceptionHandling(AggregateException ex) + { + foreach (var innerEx in ex.InnerExceptions) + Logger.WriteException("App::FinalizeExceptionHandling", innerEx); + + FinalizeExceptionHandling(ex.GetBaseException(), false); + } + + public static void FinalizeExceptionHandling(Exception ex, bool log = true) + { + if (log) + Logger.WriteException("App::FinalizeExceptionHandling", ex); + + if (_showingExceptionDialog) + return; + + _showingExceptionDialog = true; + + if (Bootstrapper?.Dialog != null) + { + if (Bootstrapper.Dialog.TaskbarProgressValue == 0) + Bootstrapper.Dialog.TaskbarProgressValue = 1; // make sure it's visible + + Bootstrapper.Dialog.TaskbarProgressState = TaskbarItemProgressState.Error; + } + + Frontend.ShowExceptionDialog(ex); + + Terminate(ErrorCode.ERROR_INSTALL_FAILURE); + } + + public static async Task GetLatestRelease() + { + const string LOG_IDENT = "App::GetLatestRelease"; + + try + { + var releaseInfo = await Http.GetJson($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); + + if (releaseInfo is null || releaseInfo.Assets is null) + { + Logger.WriteLine(LOG_IDENT, "Encountered invalid data"); + return null; + } + + return releaseInfo; + } + catch (Exception ex) + { + Logger.WriteException(LOG_IDENT, ex); + } + + return null; + } + + public static async void SendStat(string key, string value) + { + if (!Settings.Prop.EnableAnalytics) + return; + + try + { + await HttpClient.GetAsync($"https://bloxstraplabs.com/metrics/post?key={key}&value={value}"); + } + catch (Exception ex) + { + Logger.WriteException("App::SendStat", ex); + } + } + + protected override void OnStartup(StartupEventArgs e) + { + const string LOG_IDENT = "App::OnStartup"; + + Locale.Initialize(); + + base.OnStartup(e); + + Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}"); + + string userAgent = $"{ProjectName}/{Version}"; + + if (IsActionBuild) + { + Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})"); + + if (IsProductionBuild) + userAgent += $" (Production)"; + else + userAgent += $" (Artifact {BuildMetadata.CommitHash}, {BuildMetadata.CommitRef})"; + } + else + { + Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}"); + +#if QA_BUILD + userAgent += " (QA)"; +#else + userAgent += $" (Build {Convert.ToBase64String(Encoding.UTF8.GetBytes(BuildMetadata.Machine))})"; +#endif + } + + Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}"); + Logger.WriteLine(LOG_IDENT, $"Temp path is {Paths.Temp}"); + Logger.WriteLine(LOG_IDENT, $"WindowsStartMenu path is {Paths.WindowsStartMenu}"); + + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + HttpClient.Timeout = TimeSpan.FromSeconds(30); + HttpClient.DefaultRequestHeaders.Add("User-Agent", userAgent); + + LaunchSettings = new LaunchSettings(e.Args); + + // installation check begins here + using var uninstallKey = Registry.CurrentUser.OpenSubKey(UninstallKey); + string? installLocation = null; + bool fixInstallLocation = false; + + if (uninstallKey?.GetValue("InstallLocation") is string value) + { + if (Directory.Exists(value)) + { + installLocation = value; + } + else + { + // check if user profile folder has been renamed + // honestly, i'll be expecting bugs from this + var match = Regex.Match(value, @"^[a-zA-Z]:\\Users\\([^\\]+)", RegexOptions.IgnoreCase); + + if (match.Success) + { + string newLocation = value.Replace(match.Value, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase); + + if (Directory.Exists(newLocation)) + { + installLocation = newLocation; + fixInstallLocation = true; + } + } + } + } + + // silently change install location if we detect a portable run + if (installLocation is null && Directory.GetParent(Paths.Process)?.FullName is string processDir) + { + var files = Directory.GetFiles(processDir).Select(x => Path.GetFileName(x)).ToArray(); + + // check if settings.json and state.json are the only files in the folder + if (files.Length <= 3 && files.Contains("Settings.json") && files.Contains("State.json")) + { + installLocation = processDir; + fixInstallLocation = true; + } + } + + if (fixInstallLocation && installLocation is not null) + { + var installer = new Installer + { + InstallLocation = installLocation, + IsImplicitInstall = true + }; + + if (installer.CheckInstallLocation()) + { + Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'"); + installer.DoInstall(); + } + else + { + // force reinstall + installLocation = null; + } + } + + if (installLocation is null) + { + Logger.Initialize(true); + LaunchHandler.LaunchInstaller(); + } + else + { + Paths.Initialize(installLocation); + + // ensure executable is in the install directory + if (Paths.Process != Paths.Application && !File.Exists(Paths.Application)) + File.Copy(Paths.Process, Paths.Application); + + Logger.Initialize(LaunchSettings.UninstallFlag.Active); + + if (!Logger.Initialized && !Logger.NoWriteMode) + { + Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating."); + Terminate(); + } + + Settings.Load(); + State.Load(); + FastFlags.Load(); + + if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale)) + { + Settings.Prop.Locale = "nil"; + Settings.Save(); + } + + Locale.Set(Settings.Prop.Locale); + + if (!LaunchSettings.BypassUpdateCheck) + Installer.HandleUpgrade(); + + LaunchHandler.ProcessLaunchArgs(); + } + + // you must *explicitly* call terminate when everything is done, it won't be called implicitly + + FastFlags.EnsureUIFlags(); + InitializeUI(); + } + + public static void InitializeUI() + { + // Add any additional UI initialization checks here + } + } +} diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index 923c6a1..b666a4d 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -20,7 +20,14 @@ namespace Bloxstrap.AppData public override string Directory => Path.Combine(Paths.Roblox, "Player"); - public AppState State => App.State.Prop.Player; + public AppState State + { + get + { + App.InitializeUI(); + return App.State.Prop.Player; + } + } public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 73c630b..82ae344 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -14,7 +14,14 @@ public override string Directory => Path.Combine(Paths.Roblox, "Studio"); - public AppState State => App.State.Prop.Studio; + public AppState State + { + get + { + App.InitializeUI(); + return App.State.Prop.Studio; + } + } public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 284df3d..4a66f44 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -246,6 +246,14 @@ namespace Bloxstrap // TODO - remove when activity tracking has been revamped if (GetPreset("Network.Log") != "7") SetPreset("Network.Log", "7"); + + EnsureUIFlags(); + } + + public void EnsureUIFlags() + { + SetPreset("UI.Hide", null); + SetPreset("UI.FontSize", "14"); } } }