diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml deleted file mode 100644 index ae4bfac..0000000 --- a/Bloxstrap/App.xaml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - pack://application:,,,/Resources/Fonts/#Rubik Light - - - - - - - - - diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs deleted file mode 100644 index 691d868..0000000 --- a/Bloxstrap/App.xaml.cs +++ /dev/null @@ -1,359 +0,0 @@ -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 bool IsStudioVisible => !String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid); - - 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; - - SendLog(); - - 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); - } - } - - public static async void SendLog() - { - if (!Settings.Prop.EnableAnalytics || !IsProductionBuild) - return; - - try - { - await HttpClient.PostAsync( - $"https://bloxstraplabs.com/metrics/post-exception", - new StringContent(Logger.AsDocument) - ); - } - catch (Exception ex) - { - Logger.WriteException("App::SendLog", ex); - } - } - - public static void AssertWindowsOSVersion() - { - const string LOG_IDENT = "App::AssertWindowsOSVersion"; - - int major = Environment.OSVersion.Version.Major; - if (major < 10) // Windows 10 and newer only - { - Logger.WriteLine(LOG_IDENT, $"Detected unsupported Windows version ({Environment.OSVersion.Version})."); - - if (!LaunchSettings.QuietFlag.Active) - Frontend.ShowMessageBox(Strings.App_OSDeprecation_Win7_81, MessageBoxImage.Error); - - Terminate(ErrorCode.ERROR_INVALID_FUNCTION); - } - } - - 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, $"OSVersion: {Environment.OSVersion}"); - - 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 - 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); - Logger.WriteLine(LOG_IDENT, "Not installed, launching the installer"); - AssertWindowsOSVersion(); // prevent new installs from unsupported operating systems - LaunchHandler.LaunchInstaller(); - } - else - { - Paths.Initialize(installLocation); - - Logger.WriteLine(LOG_IDENT, "Entering main logic"); - - // ensure executable is in the install directory - if (Paths.Process != Paths.Application && !File.Exists(Paths.Application)) - { - Logger.WriteLine(LOG_IDENT, "Copying to install directory"); - 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 - Logger.WriteLine(LOG_IDENT, "Startup finished"); - } - } -} diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs deleted file mode 100644 index 1e1b425..0000000 --- a/Bloxstrap/AppData/CommonAppData.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.AppData -{ - public abstract class CommonAppData - { - // in case a new package is added, you can find the corresponding directory - // by opening the stock bootstrapper in a hex editor - private IReadOnlyDictionary _commonMap { get; } = new Dictionary() - { - { "Libraries.zip", @"" }, - { "redist.zip", @"" }, - { "shaders.zip", @"shaders\" }, - { "ssl.zip", @"ssl\" }, - - // the runtime installer is only extracted if it needs installing - { "WebView2.zip", @"" }, - { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, - - { "content-avatar.zip", @"content\avatar\" }, - { "content-configs.zip", @"content\configs\" }, - { "content-fonts.zip", @"content\fonts\" }, - { "content-sky.zip", @"content\sky\" }, - { "content-sounds.zip", @"content\sounds\" }, - { "content-textures2.zip", @"content\textures\" }, - { "content-models.zip", @"content\models\" }, - - { "content-textures3.zip", @"PlatformContent\pc\textures\" }, - { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, - { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, - { "content-platform-dictionaries.zip", @"PlatformContent\pc\shared_compression_dictionaries\" }, - - { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, - { "extracontent-translations.zip", @"ExtraContent\translations\" }, - { "extracontent-models.zip", @"ExtraContent\models\" }, - { "extracontent-textures.zip", @"ExtraContent\textures\" }, - { "extracontent-places.zip", @"ExtraContent\places\" }, - }; - - public virtual string ExecutableName { get; } = null!; - - public string Directory => Path.Combine(Paths.Versions, State.VersionGuid); - - public string ExecutablePath => Path.Combine(Directory, ExecutableName); - - public virtual AppState State { get; } = null!; - - public virtual IReadOnlyDictionary PackageDirectoryMap { get; set; } - - - public CommonAppData() - { - if (PackageDirectoryMap is null) - { - PackageDirectoryMap = _commonMap; - return; - } - - var merged = new Dictionary(); - - foreach (var entry in _commonMap) - merged[entry.Key] = entry.Value; - - foreach (var entry in PackageDirectoryMap) - merged[entry.Key] = entry.Value; - - PackageDirectoryMap = merged; - } - } -} diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs deleted file mode 100644 index b19aa95..0000000 --- a/Bloxstrap/AppData/IAppData.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Bloxstrap.AppData -{ - internal interface IAppData - { - string ProductName { get; } - - string BinaryType { get; } - - string RegistryName { get; } - - string ExecutableName { get; } - - string Directory { get; } - - string ExecutablePath { get; } - - AppState State { get; } - - IReadOnlyDictionary PackageDirectoryMap { get; set; } - } -} diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs deleted file mode 100644 index 3c4f728..0000000 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.AppData -{ - public class RobloxPlayerData : CommonAppData, IAppData - { - public string ProductName => "Roblox"; - - public string BinaryType => "WindowsPlayer"; - - public string RegistryName => "RobloxPlayer"; - - public override string ExecutableName => "RobloxPlayerBeta.exe"; - - public override AppState State => App.State.Prop.Player; - - public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() - { - { "RobloxApp.zip", @"" } - }; - } -} diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs deleted file mode 100644 index 2ada1c2..0000000 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Bloxstrap.AppData -{ - public class RobloxStudioData : CommonAppData, IAppData - { - public string ProductName => "Roblox Studio"; - - public string BinaryType => "WindowsStudio64"; - - public string RegistryName => "RobloxStudio"; - - public override string ExecutableName => "RobloxStudioBeta.exe"; - - public override AppState State => App.State.Prop.Studio; - - public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() - { - { "RobloxStudio.zip", @"" }, - { "LibrariesQt5.zip", @"" }, - - { "content-studio_svg_textures.zip", @"content\studio_svg_textures\"}, - { "content-qt_translations.zip", @"content\qt_translations\" }, - { "content-api-docs.zip", @"content\api_docs\" }, - - { "extracontent-scripts.zip", @"ExtraContent\scripts\" }, - - { "BuiltInPlugins.zip", @"BuiltInPlugins\" }, - { "BuiltInStandalonePlugins.zip", @"BuiltInStandalonePlugins\" }, - - { "ApplicationConfig.zip", @"ApplicationConfig\" }, - { "Plugins.zip", @"Plugins\" }, - { "Qml.zip", @"Qml\" }, - { "StudioFonts.zip", @"StudioFonts\" }, - { "RibbonConfig.zip", @"RibbonConfig\" } - }; - } -} diff --git a/Bloxstrap/AssemblyInfo.cs b/Bloxstrap/AssemblyInfo.cs deleted file mode 100644 index 8b5504e..0000000 --- a/Bloxstrap/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj deleted file mode 100644 index 8e88aa5..0000000 --- a/Bloxstrap/Bloxstrap.csproj +++ /dev/null @@ -1,114 +0,0 @@ - - - - WinExe - net6.0-windows - enable - true - True - Bloxstrap.ico - 2.9.0 - 2.9.0 - app.manifest - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - - - - - - - - - - - - - <_Parameter1>$([System.DateTime]::UtcNow.ToString("s"))Z - <_Parameter2>$(COMPUTERNAME)\$(USERNAME) - <_Parameter3>$(CommitHash) - <_Parameter4>$(CommitRef) - - - - - - - - - - - - - - - @(ProjectionMetadataWinmd->'%(FullPath)','|') - @(ProjectionDocs->'%(FullPath)','|') - - - - - - - - - - - - True - True - Strings.resx - - - - - - PublicResXFileCodeGenerator - Strings.Designer.cs - - - - diff --git a/Bloxstrap/Bloxstrap.ico b/Bloxstrap/Bloxstrap.ico deleted file mode 100644 index a9a04b9..0000000 Binary files a/Bloxstrap/Bloxstrap.ico and /dev/null differ diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs deleted file mode 100644 index da21e53..0000000 --- a/Bloxstrap/Bootstrapper.cs +++ /dev/null @@ -1,1272 +0,0 @@ -// To debug the automatic updater: -// - Uncomment the definition below -// - Publish the executable -// - Launch the executable (click no when it asks you to upgrade) -// - Launch Roblox (for testing web launches, run it from the command prompt) -// - To re-test the same executable, delete it from the installation folder - -// #define DEBUG_UPDATER - -#if DEBUG_UPDATER -#warning "Automatic updater debugging is enabled" -#endif - -using System.ComponentModel; -using System.Data; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Shell; - -using Microsoft.Win32; - -using Bloxstrap.AppData; -using Bloxstrap.RobloxInterfaces; -using Bloxstrap.UI.Elements.Bootstrapper.Base; - -using ICSharpCode.SharpZipLib.Zip; - -namespace Bloxstrap -{ - public class Bootstrapper - { - #region Properties - private const int ProgressBarMaximum = 10000; - - private const double TaskbarProgressMaximumWpf = 1; // this can not be changed. keep it at 1. - private const int TaskbarProgressMaximumWinForms = WinFormsDialogBase.TaskbarProgressMaximum; - - private const string AppSettings = - "\r\n" + - "\r\n" + - " content\r\n" + - " http://www.roblox.com\r\n" + - "\r\n"; - - private readonly FastZipEvents _fastZipEvents = new(); - private readonly CancellationTokenSource _cancelTokenSource = new(); - - private readonly IAppData AppData; - private readonly LaunchMode _launchMode; - - private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; - private string _latestVersionGuid = null!; - private string _latestVersionDirectory = null!; - private PackageManifest _versionPackageManifest = null!; - - private bool _isInstalling = false; - private double _progressIncrement; - private double _taskbarProgressIncrement; - private double _taskbarProgressMaximum; - private long _totalDownloadedBytes = 0; - - private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath); - private bool _noConnection = false; - - private AsyncMutex? _mutex; - - private int _appPid = 0; - - public IBootstrapperDialog? Dialog = null; - - public bool IsStudioLaunch => _launchMode != LaunchMode.Player; - #endregion - - #region Core - public Bootstrapper(LaunchMode launchMode) - { - _launchMode = launchMode; - - // https://github.com/icsharpcode/SharpZipLib/blob/master/src/ICSharpCode.SharpZipLib/Zip/FastZip.cs/#L669-L680 - // exceptions don't get thrown if we define events without actually binding to the failure events. probably a bug. ¯\_(ツ)_/¯ - _fastZipEvents.FileFailure += (_, e) => throw e.Exception; - _fastZipEvents.DirectoryFailure += (_, e) => throw e.Exception; - _fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested; - - AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData(); - Deployment.BinaryType = AppData.BinaryType; - } - - private void SetStatus(string message) - { - App.Logger.WriteLine("Bootstrapper::SetStatus", message); - - message = message.Replace("{product}", AppData.ProductName); - - if (Dialog is not null) - Dialog.Message = message; - } - - private void UpdateProgressBar() - { - if (Dialog is null) - return; - - // UI progress - int progressValue = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes); - - // bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100 - // too lazy to fix properly so lol - progressValue = Math.Clamp(progressValue, 0, ProgressBarMaximum); - - Dialog.ProgressValue = progressValue; - - // taskbar progress - double taskbarProgressValue = _taskbarProgressIncrement * _totalDownloadedBytes; - taskbarProgressValue = Math.Clamp(taskbarProgressValue, 0, _taskbarProgressMaximum); - - Dialog.TaskbarProgressValue = taskbarProgressValue; - } - - private void HandleConnectionError(Exception exception) - { - const string LOG_IDENT = "Bootstrapper::HandleConnectionError"; - - _noConnection = true; - - App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed"); - App.Logger.WriteException(LOG_IDENT, exception); - - string message = Strings.Dialog_Connectivity_BadConnection; - - if (exception is AggregateException) - exception = exception.InnerException!; - - // https://gist.github.com/pizzaboxer/4b58303589ee5b14cc64397460a8f386 - if (exception is HttpRequestException && exception.InnerException is null) - message = String.Format(Strings.Dialog_Connectivity_RobloxDown, "[status.roblox.com](https://status.roblox.com)"); - - if (_mustUpgrade) - message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeNeeded}\n\n{Strings.Dialog_Connectivity_TryAgainLater}"; - else - message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeSkip}"; - - Frontend.ShowConnectivityDialog( - String.Format(Strings.Dialog_Connectivity_UnableToConnect, "Roblox"), - message, - _mustUpgrade ? MessageBoxImage.Error : MessageBoxImage.Warning, - exception); - - if (_mustUpgrade) - App.Terminate(ErrorCode.ERROR_CANCELLED); - } - - public async Task Run() - { - const string LOG_IDENT = "Bootstrapper::Run"; - - App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper"); - - // this is now always enabled as of v2.8.0 - if (Dialog is not null) - Dialog.CancelEnabled = true; - - SetStatus(Strings.Bootstrapper_Status_Connecting); - - var connectionResult = await Deployment.InitializeConnectivity(); - - App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); - - if (connectionResult is not null) - HandleConnectionError(connectionResult); - -#if (!DEBUG || DEBUG_UPDATER) && !QA_BUILD - if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) - { - bool updatePresent = await CheckForUpdates(); - - if (updatePresent) - return; - } -#endif - - App.AssertWindowsOSVersion(); - - // ensure only one instance of the bootstrapper is running at the time - // so that we don't have stuff like two updates happening simultaneously - - bool mutexExists = false; - - try - { - Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close(); - App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting..."); - SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); - mutexExists = true; - } - catch (Exception) - { - // no mutex exists - } - - // wait for mutex to be released if it's not yet - await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper"); - await mutex.AcquireAsync(_cancelTokenSource.Token); - - _mutex = mutex; - - // reload our configs since they've likely changed by now - if (mutexExists) - { - App.Settings.Load(); - App.State.Load(); - } - - if (!_noConnection) - { - try - { - await GetLatestVersionInfo(); - } - catch (Exception ex) - { - HandleConnectionError(ex); - } - } - - if (!_noConnection) - { - if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) - await UpgradeRoblox(); - - if (_cancelTokenSource.IsCancellationRequested) - return; - - // we require deployment details for applying modifications for a worst case scenario, - // where we'd need to restore files from a package that isn't present on disk and needs to be redownloaded - await ApplyModifications(); - } - - // check registry entries for every launch, just in case the stock bootstrapper changes it back - - if (IsStudioLaunch) - WindowsRegistry.RegisterStudio(); - else - WindowsRegistry.RegisterPlayer(); - - if (_launchMode != LaunchMode.Player) - await mutex.ReleaseAsync(); - - if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested) - StartRoblox(); - - await mutex.ReleaseAsync(); - - Dialog?.CloseBootstrapper(); - } - - /// - /// Will throw whatever HttpClient can throw - /// - /// - private async Task GetLatestVersionInfo() - { - const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo"; - - // before we do anything, we need to query our channel - // if it's set in the launch uri, we need to use it and set the registry key for it - // else, check if the registry key for it exists, and use it - - using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel"); - - var match = Regex.Match( - App.LaunchSettings.RobloxLaunchArgs, - "channel:([a-zA-Z0-9-_]+)", - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant - ); - - if (match.Groups.Count == 2) - { - Deployment.Channel = match.Groups[1].Value.ToLowerInvariant(); - } - else if (key.GetValue("www.roblox.com") is string value && !String.IsNullOrEmpty(value)) - { - Deployment.Channel = value.ToLowerInvariant(); - } - - if (String.IsNullOrEmpty(Deployment.Channel)) - Deployment.Channel = Deployment.DefaultChannel; - - App.Logger.WriteLine(LOG_IDENT, $"Got channel as {Deployment.DefaultChannel}"); - - if (!Deployment.IsDefaultChannel) - App.SendStat("robloxChannel", Deployment.Channel); - - ClientVersion clientVersion; - - try - { - clientVersion = await Deployment.GetInfo(); - } - catch (InvalidChannelException ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}"); - - Deployment.Channel = Deployment.DefaultChannel; - clientVersion = await Deployment.GetInfo(); - } - - key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel); - - _latestVersionGuid = clientVersion.VersionGuid; - _latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid); - - string pkgManifestUrl = Deployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt"); - var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl); - - _versionPackageManifest = new(pkgManifestData); - } - - private void StartRoblox() - { - const string LOG_IDENT = "Bootstrapper::StartRoblox"; - - SetStatus(Strings.Bootstrapper_Status_Starting); - - if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage) - { - var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); - - if (match.Groups.Count == 2) - _launchCommandLine = _launchCommandLine.Replace( - "robloxLocale:en_us", - $"robloxLocale:{match.Groups[1].Value}", - StringComparison.OrdinalIgnoreCase); - } - - var startInfo = new ProcessStartInfo() - { - FileName = AppData.ExecutablePath, - Arguments = _launchCommandLine, - WorkingDirectory = AppData.Directory - }; - - if (_launchMode == LaunchMode.Player && ShouldRunAsAdmin()) - { - startInfo.Verb = "runas"; - startInfo.UseShellExecute = true; - } - else if (_launchMode == LaunchMode.StudioAuth) - { - Process.Start(startInfo); - return; - } - - string? logFileName = null; - - string rbxDir = Path.Combine(Paths.LocalAppData, "Roblox"); - if (!Directory.Exists(rbxDir)) - Directory.CreateDirectory(rbxDir); - - string rbxLogDir = Path.Combine(rbxDir, "logs"); - if (!Directory.Exists(rbxLogDir)) - Directory.CreateDirectory(rbxLogDir); - - var logWatcher = new FileSystemWatcher() - { - Path = rbxLogDir, - Filter = "*.log", - EnableRaisingEvents = true - }; - - var logCreatedEvent = new AutoResetEvent(false); - - logWatcher.Created += (_, e) => - { - logWatcher.EnableRaisingEvents = false; - logFileName = e.FullPath; - logCreatedEvent.Set(); - }; - - // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now - try - { - using var process = Process.Start(startInfo)!; - _appPid = process.Id; - } - catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) - { - // 1223 = ERROR_CANCELLED, gets thrown if a UAC prompt is cancelled - return; - } - catch (Exception) - { - // attempt a reinstall on next launch - File.Delete(AppData.ExecutablePath); - throw; - } - - App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for log file"); - - logCreatedEvent.WaitOne(TimeSpan.FromSeconds(15)); - - if (String.IsNullOrEmpty(logFileName)) - { - App.Logger.WriteLine(LOG_IDENT, "Unable to identify log file"); - Frontend.ShowPlayerErrorDialog(); - return; - } - else - { - App.Logger.WriteLine(LOG_IDENT, $"Got log file as {logFileName}"); - } - - _mutex?.ReleaseAsync(); - - if (IsStudioLaunch) - return; - - var autoclosePids = new List(); - - // launch custom integrations now - foreach (var integration in App.Settings.Prop.CustomIntegrations) - { - App.Logger.WriteLine(LOG_IDENT, $"Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})"); - - int pid = 0; - - try - { - var process = Process.Start(new ProcessStartInfo - { - FileName = integration.Location, - Arguments = integration.LaunchArgs.Replace("\r\n", " "), - WorkingDirectory = Path.GetDirectoryName(integration.Location), - UseShellExecute = true - })!; - - pid = process.Id; - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!"); - App.Logger.WriteLine(LOG_IDENT, ex.Message); - } - - if (integration.AutoClose && pid != 0) - autoclosePids.Add(pid); - } - - if (App.Settings.Prop.EnableActivityTracking || App.LaunchSettings.TestModeFlag.Active || autoclosePids.Any()) - { - using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5)); - - var watcherData = new WatcherData - { - ProcessId = _appPid, - LogFile = logFileName, - AutoclosePids = autoclosePids - }; - - string watcherDataArg = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(watcherData))); - - string args = $"-watcher \"{watcherDataArg}\""; - - if (App.LaunchSettings.TestModeFlag.Active) - args += " -testmode"; - - if (ipl.IsAcquired) - Process.Start(Paths.Process, args); - } - - // allow for window to show, since the log is created pretty far beforehand - Thread.Sleep(1000); - } - - private bool ShouldRunAsAdmin() - { - foreach (var root in WindowsRegistry.Roots) - { - using var key = root.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"); - - if (key is null) - continue; - - string? flags = (string?)key.GetValue(AppData.ExecutablePath); - - if (flags is not null && flags.Contains("RUNASADMIN", StringComparison.OrdinalIgnoreCase)) - return true; - } - - return false; - } - - public void Cancel() - { - const string LOG_IDENT = "Bootstrapper::Cancel"; - - if (_cancelTokenSource.IsCancellationRequested) - return; - - App.Logger.WriteLine(LOG_IDENT, "Cancelling launch..."); - - _cancelTokenSource.Cancel(); - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - if (_isInstalling) - { - try - { - // clean up install - if (Directory.Exists(_latestVersionDirectory)) - Directory.Delete(_latestVersionDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Could not fully clean up installation!"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - else if (_appPid != 0) - { - try - { - using var process = Process.GetProcessById(_appPid); - process.Kill(); - } - catch (Exception) { } - } - - Dialog?.CloseBootstrapper(); - - App.SoftTerminate(ErrorCode.ERROR_CANCELLED); - } -#endregion - - #region App Install - private async Task CheckForUpdates() - { - const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; - - // don't update if there's another instance running (likely running in the background) - // i don't like this, but there isn't much better way of doing it /shrug - if (Process.GetProcessesByName(App.ProjectName).Length > 1) - { - App.Logger.WriteLine(LOG_IDENT, $"More than one Bloxstrap instance running, aborting update check"); - return false; - } - - App.Logger.WriteLine(LOG_IDENT, "Checking for updates..."); - -#if !DEBUG_UPDATER - var releaseInfo = await App.GetLatestRelease(); - - if (releaseInfo is null) - return false; - - var versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName); - - // check if we aren't using a deployed build, so we can update to one if a new version comes out - if (App.IsProductionBuild && versionComparison == VersionComparison.Equal || versionComparison == VersionComparison.GreaterThan) - { - App.Logger.WriteLine(LOG_IDENT, "No updates found"); - return false; - } - - if (Dialog is not null) - Dialog.CancelEnabled = false; - - string version = releaseInfo.TagName; -#else - string version = App.Version; -#endif - - SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap); - - try - { -#if DEBUG_UPDATER - string downloadLocation = Path.Combine(Paths.TempUpdates, "Bloxstrap.exe"); - - Directory.CreateDirectory(Paths.TempUpdates); - - File.Copy(Paths.Process, downloadLocation, true); -#else - var asset = releaseInfo.Assets![0]; - - string downloadLocation = Path.Combine(Paths.TempUpdates, asset.Name); - - Directory.CreateDirectory(Paths.TempUpdates); - - App.Logger.WriteLine(LOG_IDENT, $"Downloading {releaseInfo.TagName}..."); - - if (!File.Exists(downloadLocation)) - { - var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); - - await using var fileStream = new FileStream(downloadLocation, FileMode.OpenOrCreate, FileAccess.Write); - await response.Content.CopyToAsync(fileStream); - } -#endif - - App.Logger.WriteLine(LOG_IDENT, $"Starting {version}..."); - - ProcessStartInfo startInfo = new() - { - FileName = downloadLocation, - }; - - startInfo.ArgumentList.Add("-upgrade"); - - foreach (string arg in App.LaunchSettings.Args) - startInfo.ArgumentList.Add(arg); - - if (_launchMode == LaunchMode.Player && !startInfo.ArgumentList.Contains("-player")) - startInfo.ArgumentList.Add("-player"); - else if (_launchMode == LaunchMode.Studio && !startInfo.ArgumentList.Contains("-studio")) - startInfo.ArgumentList.Add("-studio"); - - App.Settings.Save(); - - new InterProcessLock("AutoUpdater"); - - Process.Start(startInfo); - - return true; - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the auto-updater"); - App.Logger.WriteException(LOG_IDENT, ex); - - Frontend.ShowMessageBox( - string.Format(Strings.Bootstrapper_AutoUpdateFailed, version), - MessageBoxImage.Information - ); - - Utilities.ShellExecute(App.ProjectDownloadLink); - } - - return false; - } - #endregion - - #region Roblox Install - private void CleanupVersionsFolder() - { - const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder"; - - foreach (string dir in Directory.GetDirectories(Paths.Versions)) - { - string dirName = Path.GetFileName(dir); - - if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid) - { - try - { - Directory.Delete(dir, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {dir}"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } - } - - private void MigrateCompatibilityFlags() - { - const string LOG_IDENT = "Bootstrapper::MigrateCompatibilityFlags"; - - string oldClientLocation = Path.Combine(Paths.Versions, AppData.State.VersionGuid, AppData.ExecutableName); - string newClientLocation = Path.Combine(_latestVersionDirectory, AppData.ExecutableName); - - // move old compatibility flags for the old location - using RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"); - string? appFlags = appFlagsKey.GetValue(oldClientLocation) as string; - - if (appFlags is not null) - { - App.Logger.WriteLine(LOG_IDENT, $"Migrating app compatibility flags from {oldClientLocation} to {newClientLocation}..."); - appFlagsKey.SetValueSafe(newClientLocation, appFlags); - appFlagsKey.DeleteValueSafe(oldClientLocation); - } - } - - private static void KillRobloxPlayers() - { - const string LOG_IDENT = "Bootstrapper::KillRobloxPlayers"; - - List processes = new List(); - processes.AddRange(Process.GetProcessesByName("RobloxPlayerBeta")); - processes.AddRange(Process.GetProcessesByName("RobloxCrashHandler")); // roblox studio doesnt depend on crash handler being open, so this should be fine - - foreach (Process process in processes) - { - try - { - process.Kill(); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to close process {process.Id}"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } - - private async Task UpgradeRoblox() - { - const string LOG_IDENT = "Bootstrapper::UpgradeRoblox"; - - if (String.IsNullOrEmpty(AppData.State.VersionGuid)) - SetStatus(Strings.Bootstrapper_Status_Installing); - else - SetStatus(Strings.Bootstrapper_Status_Upgrading); - - Directory.CreateDirectory(Paths.Base); - Directory.CreateDirectory(Paths.Downloads); - Directory.CreateDirectory(Paths.Versions); - - _isInstalling = true; - - // make sure nothing is running before continuing upgrade - if (!IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss - KillRobloxPlayers(); - - // get a fully clean install - if (Directory.Exists(_latestVersionDirectory)) - { - try - { - Directory.Delete(_latestVersionDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to delete the latest version directory"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - - Directory.CreateDirectory(_latestVersionDirectory); - - var cachedPackageHashes = Directory.GetFiles(Paths.Downloads).Select(x => Path.GetFileName(x)); - - // package manifest states packed size and uncompressed size in exact bytes - int totalSizeRequired = 0; - - // packed size only matters if we don't already have the package cached on disk - totalSizeRequired += _versionPackageManifest.Where(x => !cachedPackageHashes.Contains(x.Signature)).Sum(x => x.PackedSize); - totalSizeRequired += _versionPackageManifest.Sum(x => x.Size); - - if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) - { - Frontend.ShowMessageBox(Strings.Bootstrapper_NotEnoughSpace, MessageBoxImage.Error); - App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); - return; - } - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Continuous; - Dialog.TaskbarProgressState = TaskbarItemProgressState.Normal; - - Dialog.ProgressMaximum = ProgressBarMaximum; - - // compute total bytes to download - int totalPackedSize = _versionPackageManifest.Sum(package => package.PackedSize); - _progressIncrement = (double)ProgressBarMaximum / totalPackedSize; - - if (Dialog is WinFormsDialogBase) - _taskbarProgressMaximum = (double)TaskbarProgressMaximumWinForms; - else - _taskbarProgressMaximum = (double)TaskbarProgressMaximumWpf; - - _taskbarProgressIncrement = _taskbarProgressMaximum / (double)totalPackedSize; - } - - var extractionTasks = new List(); - - foreach (var package in _versionPackageManifest) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - // download all the packages synchronously - await DownloadPackage(package); - - // we'll extract the runtime installer later if we need to - if (package.Name == "WebView2RuntimeInstaller.zip") - continue; - - // extract the package async immediately after download - extractionTasks.Add(Task.Run(() => ExtractPackage(package), _cancelTokenSource.Token)); - } - - if (_cancelTokenSource.IsCancellationRequested) - return; - - if (Dialog is not null) - { - Dialog.ProgressStyle = ProgressBarStyle.Marquee; - Dialog.TaskbarProgressState = TaskbarItemProgressState.Indeterminate; - SetStatus(Strings.Bootstrapper_Status_Configuring); - } - - await Task.WhenAll(extractionTasks); - - App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); - await File.WriteAllTextAsync(Path.Combine(_latestVersionDirectory, "AppSettings.xml"), AppSettings); - - if (_cancelTokenSource.IsCancellationRequested) - return; - - if (App.State.Prop.PromptWebView2Install) - { - using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - - if (hklmKey is not null || hkcuKey is not null) - { - // reset prompt state if the user has it installed - App.State.Prop.PromptWebView2Install = true; - } - else - { - var result = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes); - - if (result != MessageBoxResult.Yes) - { - App.State.Prop.PromptWebView2Install = false; - } - else - { - App.Logger.WriteLine(LOG_IDENT, "Installing WebView2 runtime..."); - - var package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip"); - - if (package is null) - { - App.Logger.WriteLine(LOG_IDENT, "Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?"); - return; - } - - string baseDirectory = Path.Combine(_latestVersionDirectory, AppData.PackageDirectoryMap[package.Name]); - - ExtractPackage(package); - - SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); - - var startInfo = new ProcessStartInfo() - { - WorkingDirectory = baseDirectory, - FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"), - Arguments = "/silent /install" - }; - - await Process.Start(startInfo)!.WaitForExitAsync(); - - App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); - - Directory.Delete(baseDirectory, true); - } - } - } - - // finishing and cleanup - - MigrateCompatibilityFlags(); - - AppData.State.VersionGuid = _latestVersionGuid; - - AppData.State.PackageHashes.Clear(); - - foreach (var package in _versionPackageManifest) - AppData.State.PackageHashes.Add(package.Name, package.Signature); - - CleanupVersionsFolder(); - - var allPackageHashes = new List(); - - allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values); - allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values); - - foreach (string hash in cachedPackageHashes) - { - if (!allPackageHashes.Contains(hash)) - { - App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}"); - - try - { - File.Delete(Path.Combine(Paths.Downloads, hash)); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } - - App.Logger.WriteLine(LOG_IDENT, "Registering approximate program size..."); - - int distributionSize = _versionPackageManifest.Sum(x => x.Size + x.PackedSize) / 1024; - - AppData.State.Size = distributionSize; - - int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size; - - using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) - { - uninstallKey.SetValueSafe("EstimatedSize", totalSize); - } - - App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB"); - - App.State.Save(); - - _isInstalling = false; - } - - private async Task ApplyModifications() - { - const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - - SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); - - // handle file mods - App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); - - // manifest has been moved to State.json - File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); - - List modFolderFiles = new(); - - Directory.CreateDirectory(Paths.Modifications); - - // check custom font mod - // instead of replacing the fonts themselves, we'll just alter the font family manifests - - string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - - if (File.Exists(Paths.CustomFont)) - { - App.Logger.WriteLine(LOG_IDENT, "Begin font check"); - - Directory.CreateDirectory(modFontFamiliesFolder); - - const string path = "rbxasset://fonts/CustomFont.ttf"; - - // lets make sure the content/fonts/families path exists in the version directory - string contentFolder = Path.Combine(_latestVersionDirectory, "content"); - Directory.CreateDirectory(contentFolder); - - string fontsFolder = Path.Combine(contentFolder, "fonts"); - Directory.CreateDirectory(fontsFolder); - - string familiesFolder = Path.Combine(fontsFolder, "families"); - Directory.CreateDirectory(familiesFolder); - - foreach (string jsonFilePath in Directory.GetFiles(familiesFolder)) - { - string jsonFilename = Path.GetFileName(jsonFilePath); - string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); - - if (File.Exists(modFilepath)) - continue; - - App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); - - var fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); - - if (fontFamilyData is null) - continue; - - bool shouldWrite = false; - - foreach (var fontFace in fontFamilyData.Faces) - { - if (fontFace.AssetId != path) - { - fontFace.AssetId = path; - shouldWrite = true; - } - } - - if (shouldWrite) - File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); - } - - App.Logger.WriteLine(LOG_IDENT, "End font check"); - } - else if (Directory.Exists(modFontFamiliesFolder)) - { - Directory.Delete(modFontFamiliesFolder, true); - } - - foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - // get relative directory path - string relativeFile = file.Substring(Paths.Modifications.Length + 1); - - // v1.7.0 - README has been moved to the preferences menu now - if (relativeFile == "README.txt") - { - File.Delete(file); - continue; - } - - if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) - continue; - - if (relativeFile.EndsWith(".lock")) - continue; - - modFolderFiles.Add(relativeFile); - - string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); - string fileVersionFolder = Path.Combine(_latestVersionDirectory, relativeFile); - - if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) - { - App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); - continue; - } - - Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); - - Filesystem.AssertReadOnly(fileVersionFolder); - File.Copy(fileModFolder, fileVersionFolder, true); - 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 - // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages - // now check for files that have been deleted from the mod folder according to the manifest - - var fileRestoreMap = new Dictionary>(); - - foreach (string fileLocation in App.State.Prop.ModManifest) - { - if (modFolderFiles.Contains(fileLocation)) - continue; - - var packageMapEntry = AppData.PackageDirectoryMap.SingleOrDefault(x => !String.IsNullOrEmpty(x.Value) && fileLocation.StartsWith(x.Value)); - string packageName = packageMapEntry.Key; - - // package doesn't exist, likely mistakenly placed file - if (String.IsNullOrEmpty(packageName)) - { - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); - - string versionFileLocation = Path.Combine(_latestVersionDirectory, fileLocation); - - if (File.Exists(versionFileLocation)) - File.Delete(versionFileLocation); - - continue; - } - - string fileName = fileLocation.Substring(packageMapEntry.Value.Length); - - if (!fileRestoreMap.ContainsKey(packageName)) - fileRestoreMap[packageName] = new(); - - fileRestoreMap[packageName].Add(fileName); - - App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restoring from {packageName}"); - } - - foreach (var entry in fileRestoreMap) - { - var package = _versionPackageManifest.Find(x => x.Name == entry.Key); - - if (package is not null) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - await DownloadPackage(package); - ExtractPackage(package, entry.Value); - } - } - - App.State.Prop.ModManifest = modFolderFiles; - App.State.Save(); - - App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); - } - - private async Task DownloadPackage(Package package) - { - string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}"; - - if (_cancelTokenSource.IsCancellationRequested) - return; - - Directory.CreateDirectory(Paths.Downloads); - - string packageUrl = Deployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature); - - if (File.Exists(package.DownloadPath)) - { - var file = new FileInfo(package.DownloadPath); - - string calculatedMD5 = MD5Hash.FromFile(package.DownloadPath); - - if (calculatedMD5 != package.Signature) - { - App.Logger.WriteLine(LOG_IDENT, $"Package is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); - file.Delete(); - } - else - { - App.Logger.WriteLine(LOG_IDENT, $"Package is already downloaded, skipping..."); - - _totalDownloadedBytes += package.PackedSize; - UpdateProgressBar(); - - return; - } - } - else if (File.Exists(robloxPackageLocation)) - { - // let's cheat! if the stock bootstrapper already previously downloaded the file, - // then we can just copy the one from there - - App.Logger.WriteLine(LOG_IDENT, $"Found existing copy at '{robloxPackageLocation}'! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, package.DownloadPath); - - _totalDownloadedBytes += package.PackedSize; - UpdateProgressBar(); - - return; - } - - if (File.Exists(package.DownloadPath)) - return; - - const int maxTries = 5; - - App.Logger.WriteLine(LOG_IDENT, "Downloading..."); - - var buffer = new byte[4096]; - - for (int i = 1; i <= maxTries; i++) - { - if (_cancelTokenSource.IsCancellationRequested) - return; - - int totalBytesRead = 0; - - try - { - var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); - await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(package.DownloadPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); - - while (true) - { - if (_cancelTokenSource.IsCancellationRequested) - { - stream.Close(); - fileStream.Close(); - return; - } - - int bytesRead = await stream.ReadAsync(buffer, _cancelTokenSource.Token); - - if (bytesRead == 0) - break; - - totalBytesRead += bytesRead; - - await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), _cancelTokenSource.Token); - - _totalDownloadedBytes += bytesRead; - UpdateProgressBar(); - } - - string hash = MD5Hash.FromStream(fileStream); - - if (hash != package.Signature) - throw new ChecksumFailedException($"Failed to verify download of {packageUrl}\n\nExpected hash: {package.Signature}\nGot hash: {hash}"); - - App.Logger.WriteLine(LOG_IDENT, $"Finished downloading! ({totalBytesRead} bytes total)"); - break; - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})"); - App.Logger.WriteException(LOG_IDENT, ex); - - if (ex.GetType() == typeof(ChecksumFailedException)) - { - App.SendStat("packageDownloadState", "httpFail"); - - Frontend.ShowConnectivityDialog( - Strings.Dialog_Connectivity_UnableToDownload, - String.Format(Strings.Dialog_Connectivity_UnableToDownloadReason, "[https://github.com/bloxstraplabs/bloxstrap/wiki/Bloxstrap-is-unable-to-download-Roblox](https://github.com/bloxstraplabs/bloxstrap/wiki/Bloxstrap-is-unable-to-download-Roblox)"), - MessageBoxImage.Error, - ex - ); - - App.Terminate(ErrorCode.ERROR_CANCELLED); - } - else if (i >= maxTries) - throw; - - if (File.Exists(package.DownloadPath)) - File.Delete(package.DownloadPath); - - _totalDownloadedBytes -= totalBytesRead; - UpdateProgressBar(); - - // attempt download over HTTP - // this isn't actually that unsafe - signatures were fetched earlier over HTTPS - // so we've already established that our signatures are legit, and that there's very likely no MITM anyway - if (ex.GetType() == typeof(IOException) && !packageUrl.StartsWith("http://")) - { - App.Logger.WriteLine(LOG_IDENT, "Retrying download over HTTP..."); - packageUrl = packageUrl.Replace("https://", "http://"); - } - } - } - } - - private void ExtractPackage(Package package, List? files = null) - { - const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - - string? packageDir = AppData.PackageDirectoryMap.GetValueOrDefault(package.Name); - - if (packageDir is null) - { - App.Logger.WriteLine(LOG_IDENT, $"WARNING: {package.Name} was not found in the package map!"); - return; - } - - string packageFolder = Path.Combine(_latestVersionDirectory, packageDir); - string? fileFilter = null; - - // for sharpziplib, each file in the filter needs to be a regex - if (files is not null) - { - var regexList = new List(); - - foreach (string file in files) - regexList.Add("^" + file.Replace("\\", "\\\\") + "$"); - - fileFilter = String.Join(';', regexList); - } - - App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); - - var fastZip = new FastZip(_fastZipEvents); - - fastZip.ExtractZip(package.DownloadPath, packageFolder, fileFilter); - - App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); - } - #endregion - } -} diff --git a/Bloxstrap/Enums/BootstrapperIcon.cs b/Bloxstrap/Enums/BootstrapperIcon.cs deleted file mode 100644 index 4125cc6..0000000 --- a/Bloxstrap/Enums/BootstrapperIcon.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum BootstrapperIcon - { - [EnumName(StaticName = "Bloxstrap")] - IconBloxstrap, - [EnumName(StaticName = "2008")] - Icon2008, - [EnumName(StaticName = "2011")] - Icon2011, - IconEarly2015, - IconLate2015, - [EnumName(StaticName = "2017")] - Icon2017, - [EnumName(StaticName = "2019")] - Icon2019, - [EnumName(StaticName = "2022")] - Icon2022, - [EnumName(FromTranslation = "Common.Custom")] - IconCustom, - [EnumName(FromTranslation = "Enums.BootstrapperStyle.ClassicFluentDialog")] - IconBloxstrapClassic - } -} diff --git a/Bloxstrap/Enums/BootstrapperStyle.cs b/Bloxstrap/Enums/BootstrapperStyle.cs deleted file mode 100644 index 5c5f6fd..0000000 --- a/Bloxstrap/Enums/BootstrapperStyle.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum BootstrapperStyle - { - VistaDialog, - LegacyDialog2008, - LegacyDialog2011, - ProgressDialog, - ClassicFluentDialog, - ByfronDialog, - [EnumName(StaticName = "Bloxstrap")] - FluentDialog, - FluentAeroDialog - } -} diff --git a/Bloxstrap/Enums/CursorType.cs b/Bloxstrap/Enums/CursorType.cs deleted file mode 100644 index 8b0fa60..0000000 --- a/Bloxstrap/Enums/CursorType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum CursorType - { - [EnumSort(Order = 1)] - [EnumName(FromTranslation = "Common.Default")] - Default, - - [EnumSort(Order = 3)] - From2006, - - [EnumSort(Order = 2)] - From2013 - } -} diff --git a/Bloxstrap/Enums/EmojiType.cs b/Bloxstrap/Enums/EmojiType.cs deleted file mode 100644 index cdefefd..0000000 --- a/Bloxstrap/Enums/EmojiType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum EmojiType - { - Default, - Catmoji, - Windows11, - Windows10, - Windows8 - } -} diff --git a/Bloxstrap/Enums/ErrorCode.cs b/Bloxstrap/Enums/ErrorCode.cs deleted file mode 100644 index 4d830dd..0000000 --- a/Bloxstrap/Enums/ErrorCode.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Bloxstrap.Enums -{ - // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes - // https://i-logic.com/serial/errorcodes.htm - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/705fb797-2175-4a90-b5a3-3918024b10b8 - // just the ones that we're interested in - - public enum ErrorCode - { - ERROR_SUCCESS = 0, - ERROR_INVALID_FUNCTION = 1, - ERROR_FILE_NOT_FOUND = 2, - - ERROR_CANCELLED = 1223, - ERROR_INSTALL_USEREXIT = 1602, - ERROR_INSTALL_FAILURE = 1603, - - CO_E_APPNOTFOUND = -2147221003 - } -} diff --git a/Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs b/Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs deleted file mode 100644 index 30bbc70..0000000 --- a/Bloxstrap/Enums/FlagPresets/InGameMenuVersion.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum InGameMenuVersion - { - [EnumName(FromTranslation = "Common.Default")] - Default, - V1, - V2, - V4, - V4Chrome - } -} diff --git a/Bloxstrap/Enums/FlagPresets/LightingMode.cs b/Bloxstrap/Enums/FlagPresets/LightingMode.cs deleted file mode 100644 index 88ba4da..0000000 --- a/Bloxstrap/Enums/FlagPresets/LightingMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum LightingMode - { - Default, - Voxel, - ShadowMap, - Future - } -} diff --git a/Bloxstrap/Enums/FlagPresets/MSAAMode.cs b/Bloxstrap/Enums/FlagPresets/MSAAMode.cs deleted file mode 100644 index e5ae281..0000000 --- a/Bloxstrap/Enums/FlagPresets/MSAAMode.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum MSAAMode - { - [EnumName(FromTranslation = "Common.Automatic")] - Default, - [EnumName(StaticName = "1x")] - x1, - [EnumName(StaticName = "2x")] - x2, - [EnumName(StaticName = "4x")] - x4 - } -} diff --git a/Bloxstrap/Enums/FlagPresets/RenderingMode.cs b/Bloxstrap/Enums/FlagPresets/RenderingMode.cs deleted file mode 100644 index 082d301..0000000 --- a/Bloxstrap/Enums/FlagPresets/RenderingMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum RenderingMode - { - [EnumName(FromTranslation = "Common.Automatic")] - Default, - D3D11, - D3D10, - } -} diff --git a/Bloxstrap/Enums/FlagPresets/TextureQuality.cs b/Bloxstrap/Enums/FlagPresets/TextureQuality.cs deleted file mode 100644 index 9610135..0000000 --- a/Bloxstrap/Enums/FlagPresets/TextureQuality.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Bloxstrap.Enums.FlagPresets -{ - public enum TextureQuality - { - [EnumName(FromTranslation = "Common.Automatic")] - Default, - Level0, - Level1, - Level2, - Level3 - } -} diff --git a/Bloxstrap/Enums/GenericTriState.cs b/Bloxstrap/Enums/GenericTriState.cs deleted file mode 100644 index c8b406b..0000000 --- a/Bloxstrap/Enums/GenericTriState.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum GenericTriState - { - Successful, - Failed, - Unknown - } -} diff --git a/Bloxstrap/Enums/LaunchMode.cs b/Bloxstrap/Enums/LaunchMode.cs deleted file mode 100644 index c045d4f..0000000 --- a/Bloxstrap/Enums/LaunchMode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum LaunchMode - { - None, - Player, - Studio, - StudioAuth - } -} diff --git a/Bloxstrap/Enums/NextAction.cs b/Bloxstrap/Enums/NextAction.cs deleted file mode 100644 index 607029e..0000000 --- a/Bloxstrap/Enums/NextAction.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum NextAction - { - Terminate, - LaunchSettings, - LaunchRoblox, - LaunchRobloxStudio - } -} diff --git a/Bloxstrap/Enums/ServerType.cs b/Bloxstrap/Enums/ServerType.cs deleted file mode 100644 index 70386d6..0000000 --- a/Bloxstrap/Enums/ServerType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum ServerType - { - Public, - Private, - Reserved - } -} diff --git a/Bloxstrap/Enums/Theme.cs b/Bloxstrap/Enums/Theme.cs deleted file mode 100644 index f3cd71c..0000000 --- a/Bloxstrap/Enums/Theme.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Enums -{ - public enum Theme - { - [EnumName(FromTranslation = "Common.SystemDefault")] - Default, - Light, - Dark - } -} diff --git a/Bloxstrap/Enums/VersionComparison.cs b/Bloxstrap/Enums/VersionComparison.cs deleted file mode 100644 index 8f65958..0000000 --- a/Bloxstrap/Enums/VersionComparison.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bloxstrap.Enums -{ - enum VersionComparison - { - LessThan = -1, - Equal = 0, - GreaterThan = 1 - } -} diff --git a/Bloxstrap/Exceptions/AssertionException.cs b/Bloxstrap/Exceptions/AssertionException.cs deleted file mode 100644 index 5555baa..0000000 --- a/Bloxstrap/Exceptions/AssertionException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Exceptions -{ - internal class AssertionException : Exception - { - public AssertionException(string message) - : base($"{message}\n\nThis is very likely just an off-chance error. Please report this first, and then start {App.ProjectName} again.") - { - } - } -} diff --git a/Bloxstrap/Exceptions/ChecksumFailedException.cs b/Bloxstrap/Exceptions/ChecksumFailedException.cs deleted file mode 100644 index 95d8af2..0000000 --- a/Bloxstrap/Exceptions/ChecksumFailedException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Exceptions -{ - internal class ChecksumFailedException : Exception - { - public ChecksumFailedException(string message) : base(message) - { - } - } -} diff --git a/Bloxstrap/Exceptions/InvalidChannelException.cs b/Bloxstrap/Exceptions/InvalidChannelException.cs deleted file mode 100644 index eff6d79..0000000 --- a/Bloxstrap/Exceptions/InvalidChannelException.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Exceptions -{ - public class InvalidChannelException : Exception - { - public HttpStatusCode? StatusCode; - - public InvalidChannelException(HttpStatusCode? statusCode) : base() - => StatusCode = statusCode; - } -} diff --git a/Bloxstrap/Exceptions/InvalidHTTPResponseException.cs b/Bloxstrap/Exceptions/InvalidHTTPResponseException.cs deleted file mode 100644 index f6b45a0..0000000 --- a/Bloxstrap/Exceptions/InvalidHTTPResponseException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Bloxstrap.Exceptions -{ - internal class InvalidHTTPResponseException : Exception - { - public InvalidHTTPResponseException(string message) : base(message) { } - } -} diff --git a/Bloxstrap/Extensions/BootstrapperIconEx.cs b/Bloxstrap/Extensions/BootstrapperIconEx.cs deleted file mode 100644 index 943cad6..0000000 --- a/Bloxstrap/Extensions/BootstrapperIconEx.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Drawing; - -namespace Bloxstrap.Extensions -{ - static class BootstrapperIconEx - { - public static IReadOnlyCollection Selections => new BootstrapperIcon[] - { - BootstrapperIcon.IconBloxstrap, - BootstrapperIcon.Icon2022, - BootstrapperIcon.Icon2019, - BootstrapperIcon.Icon2017, - BootstrapperIcon.IconLate2015, - BootstrapperIcon.IconEarly2015, - BootstrapperIcon.Icon2011, - BootstrapperIcon.Icon2008, - BootstrapperIcon.IconBloxstrapClassic, - BootstrapperIcon.IconCustom - }; - - // small note on handling icon sizes - // i'm using multisize icon packs here with sizes 16, 24, 32, 48, 64 and 128 - // use this for generating multisize packs: https://www.aconvert.com/icon/ - - public static Icon GetIcon(this BootstrapperIcon icon) - { - const string LOG_IDENT = "BootstrapperIconEx::GetIcon"; - - // load the custom icon file - if (icon == BootstrapperIcon.IconCustom) - { - Icon? customIcon = null; - string location = App.Settings.Prop.BootstrapperIconCustomLocation; - - if (String.IsNullOrEmpty(location)) - { - App.Logger.WriteLine(LOG_IDENT, "Warning: custom icon is not set."); - } - else - { - try - { - customIcon = new Icon(location); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to load custom icon!"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - - return customIcon ?? Properties.Resources.IconBloxstrap; - } - - return icon switch - { - BootstrapperIcon.IconBloxstrap => Properties.Resources.IconBloxstrap, - BootstrapperIcon.Icon2008 => Properties.Resources.Icon2008, - BootstrapperIcon.Icon2011 => Properties.Resources.Icon2011, - BootstrapperIcon.IconEarly2015 => Properties.Resources.IconEarly2015, - BootstrapperIcon.IconLate2015 => Properties.Resources.IconLate2015, - BootstrapperIcon.Icon2017 => Properties.Resources.Icon2017, - BootstrapperIcon.Icon2019 => Properties.Resources.Icon2019, - BootstrapperIcon.Icon2022 => Properties.Resources.Icon2022, - BootstrapperIcon.IconBloxstrapClassic => Properties.Resources.IconBloxstrapClassic, - _ => Properties.Resources.IconBloxstrap - }; - } - } -} diff --git a/Bloxstrap/Extensions/BootstrapperStyleEx.cs b/Bloxstrap/Extensions/BootstrapperStyleEx.cs deleted file mode 100644 index 3802264..0000000 --- a/Bloxstrap/Extensions/BootstrapperStyleEx.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Bloxstrap.Extensions -{ - static class BootstrapperStyleEx - { - public static IBootstrapperDialog GetNew(this BootstrapperStyle bootstrapperStyle) => Frontend.GetBootstrapperDialog(bootstrapperStyle); - - public static IReadOnlyCollection Selections => new BootstrapperStyle[] - { - BootstrapperStyle.FluentDialog, - BootstrapperStyle.FluentAeroDialog, - BootstrapperStyle.ClassicFluentDialog, - BootstrapperStyle.ByfronDialog, - BootstrapperStyle.ProgressDialog, - BootstrapperStyle.LegacyDialog2011, - BootstrapperStyle.LegacyDialog2008, - BootstrapperStyle.VistaDialog - }; - } -} diff --git a/Bloxstrap/Extensions/DateTimeEx.cs b/Bloxstrap/Extensions/DateTimeEx.cs deleted file mode 100644 index 0dc8a24..0000000 --- a/Bloxstrap/Extensions/DateTimeEx.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bloxstrap.Extensions -{ - static class DateTimeEx - { - public static string ToFriendlyString(this DateTime dateTime) - { - return dateTime.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", CultureInfo.InvariantCulture); - } - } -} diff --git a/Bloxstrap/Extensions/EmojiTypeEx.cs b/Bloxstrap/Extensions/EmojiTypeEx.cs deleted file mode 100644 index e1b32fd..0000000 --- a/Bloxstrap/Extensions/EmojiTypeEx.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Bloxstrap.Extensions -{ - static class EmojiTypeEx - { - public static IReadOnlyDictionary Filenames => new Dictionary - { - { EmojiType.Catmoji, "Catmoji.ttf" }, - { EmojiType.Windows11, "Win1122H2SegoeUIEmoji.ttf" }, - { EmojiType.Windows10, "Win10April2018SegoeUIEmoji.ttf" }, - { EmojiType.Windows8, "Win8.1SegoeUIEmoji.ttf" }, - }; - - public static IReadOnlyDictionary Hashes => new Dictionary - { - { EmojiType.Catmoji, "98138f398a8cde897074dd2b8d53eca0" }, - { EmojiType.Windows11, "d50758427673578ddf6c9edcdbf367f5" }, - { EmojiType.Windows10, "d8a7eecbebf9dfdf622db8ccda63aff5" }, - { EmojiType.Windows8, "2b01c6caabbe95afc92aa63b9bf100f3" }, - }; - - public static string GetHash(this EmojiType emojiType) => Hashes[emojiType]; - - public static string GetUrl(this EmojiType emojiType) - { - if (emojiType == EmojiType.Default) - return ""; - - return $"https://github.com/bloxstraplabs/rbxcustom-fontemojis/releases/download/my-phone-is-78-percent/{Filenames[emojiType]}"; - } - } -} diff --git a/Bloxstrap/Extensions/IconEx.cs b/Bloxstrap/Extensions/IconEx.cs deleted file mode 100644 index 516899d..0000000 --- a/Bloxstrap/Extensions/IconEx.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Drawing; -using System.Windows.Media.Imaging; -using System.Windows.Media; - -namespace Bloxstrap.Extensions -{ - public static class IconEx - { - public static Icon GetSized(this Icon icon, int width, int height) => new(icon, new Size(width, height)); - - public static ImageSource GetImageSource(this Icon icon, bool handleException = true) - { - using MemoryStream stream = new(); - icon.Save(stream); - - if (handleException) - { - try - { - return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); - } - catch (Exception ex) - { - App.Logger.WriteException("IconEx::GetImageSource", ex); - Frontend.ShowMessageBox(String.Format(Strings.Dialog_IconLoadFailed, ex.Message)); - return BootstrapperIcon.IconBloxstrap.GetIcon().GetImageSource(false); - } - } - else - { - return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); - } - } - } -} diff --git a/Bloxstrap/Extensions/RegistryKeyEx.cs b/Bloxstrap/Extensions/RegistryKeyEx.cs deleted file mode 100644 index 19efd58..0000000 --- a/Bloxstrap/Extensions/RegistryKeyEx.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Win32; - -namespace Bloxstrap.Extensions -{ - public static class RegistryKeyEx - { - public static void SetValueSafe(this RegistryKey registryKey, string? name, object value) - { - try - { - App.Logger.WriteLine("RegistryKeyEx::SetValueSafe", $"Writing '{value}' to {registryKey}\\{name}"); - registryKey.SetValue(name, value); - } - catch (UnauthorizedAccessException) - { - Frontend.ShowMessageBox(Strings.Dialog_RegistryWriteError, System.Windows.MessageBoxImage.Error); - App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); - } - } - - public static void DeleteValueSafe(this RegistryKey registryKey, string name) - { - try - { - App.Logger.WriteLine("RegistryKeyEx::DeleteValueSafe", $"Deleting {registryKey}\\{name}"); - registryKey.DeleteValue(name); - } - catch (UnauthorizedAccessException) - { - Frontend.ShowMessageBox(Strings.Dialog_RegistryWriteError, System.Windows.MessageBoxImage.Error); - App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); - } - } - } -} diff --git a/Bloxstrap/Extensions/ResourceManagerEx.cs b/Bloxstrap/Extensions/ResourceManagerEx.cs deleted file mode 100644 index def61c2..0000000 --- a/Bloxstrap/Extensions/ResourceManagerEx.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Resources; - -namespace Bloxstrap.Extensions -{ - static class ResourceManagerEx - { - /// - /// Returns the value of the specified string resource.
- /// If the resource is not found, the resource name will be returned. - ///
- public static string GetStringSafe(this ResourceManager manager, string name) => manager.GetStringSafe(name, null); - - /// - /// Returns the value of the string resource localized for the specified culture.
- /// If the resource is not found, the resource name will be returned. - ///
- public static string GetStringSafe(this ResourceManager manager, string name, CultureInfo? culture) - { - string? resourceValue = manager.GetString(name, culture); - - return resourceValue ?? name; - } - } -} diff --git a/Bloxstrap/Extensions/ServerTypeEx.cs b/Bloxstrap/Extensions/ServerTypeEx.cs deleted file mode 100644 index 2b65d8b..0000000 --- a/Bloxstrap/Extensions/ServerTypeEx.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Bloxstrap.Extensions -{ - static class ServerTypeEx - { - public static string ToTranslatedString(this ServerType value) => value switch - { - ServerType.Public => Strings.Enums_ServerType_Public, - ServerType.Private => Strings.Enums_ServerType_Private, - ServerType.Reserved => Strings.Enums_ServerType_Reserved, - _ => "?" - }; - } -} diff --git a/Bloxstrap/Extensions/ThemeEx.cs b/Bloxstrap/Extensions/ThemeEx.cs deleted file mode 100644 index f5fc70c..0000000 --- a/Bloxstrap/Extensions/ThemeEx.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Win32; - -namespace Bloxstrap.Extensions -{ - public static class ThemeEx - { - public static Theme GetFinal(this Theme dialogTheme) - { - if (dialogTheme != Theme.Default) - return dialogTheme; - - using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); - - if (key?.GetValue("AppsUseLightTheme") is int value && value == 0) - return Theme.Dark; - - return Theme.Light; - } - } -} diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs deleted file mode 100644 index a69b3e5..0000000 --- a/Bloxstrap/FastFlagManager.cs +++ /dev/null @@ -1,263 +0,0 @@ -using Bloxstrap.Enums.FlagPresets; - -namespace Bloxstrap -{ - public class FastFlagManager : JsonManager> - { - public override string ClassName => nameof(FastFlagManager); - - public override string LOG_IDENT_CLASS => ClassName; - - public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json"); - - public bool Changed => !OriginalProp.SequenceEqual(Prop); - - public static IReadOnlyDictionary PresetFlags = new Dictionary - { - { "Network.Log", "FLogNetwork" }, - - { "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" }, - { "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" }, - { "Rendering.DisableScaling", "DFFlagDisableDPIScale" }, - { "Rendering.MSAA", "FIntDebugForceMSAASamples" }, - { "Rendering.DisablePostFX", "FFlagDisablePostFx" }, - { "Rendering.ShadowIntensity", "FIntRenderShadowIntensity" }, - - { "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" }, - { "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" }, - - { "Rendering.Lighting.Voxel", "DFFlagDebugRenderForceTechnologyVoxel" }, - { "Rendering.Lighting.ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" }, - { "Rendering.Lighting.Future", "FFlagDebugForceFutureIsBrightPhase3" }, - - { "Rendering.TextureQuality.OverrideEnabled", "DFFlagTextureQualityOverrideEnabled" }, - { "Rendering.TextureQuality.Level", "DFIntTextureQualityOverride" }, - { "Rendering.TerrainTextureQuality", "FIntTerrainArraySliceSize" }, - - { "UI.Hide", "DFIntCanHideGuiGroupId" }, - { "UI.FontSize", "FIntFontSizePadding" }, - - { "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" }, - - //{ "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" }, - //{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" }, - //{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" }, - //{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" }, - //{ "UI.Menu.Style.ReportButtonCutOff", "FFlagFixReportButtonCutOff" }, - - - //{ "UI.Menu.Style.ABTest.1", "FFlagEnableMenuControlsABTest" }, - //{ "UI.Menu.Style.ABTest.2", "FFlagEnableV3MenuABTest3" }, - //{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" }, - //{ "UI.Menu.Style.ABTest.4", "FFlagEnableInGameMenuChromeABTest4" } - }; - - public static IReadOnlyDictionary RenderingModes => new Dictionary - { - { RenderingMode.Default, "None" }, - { RenderingMode.D3D11, "D3D11" }, - { RenderingMode.D3D10, "D3D10" }, - }; - - public static IReadOnlyDictionary LightingModes => new Dictionary - { - { LightingMode.Default, "None" }, - { LightingMode.Voxel, "Voxel" }, - { LightingMode.ShadowMap, "ShadowMap" }, - { LightingMode.Future, "Future" } - }; - - public static IReadOnlyDictionary MSAAModes => new Dictionary - { - { MSAAMode.Default, null }, - { MSAAMode.x1, "1" }, - { MSAAMode.x2, "2" }, - { MSAAMode.x4, "4" } - }; - - public static IReadOnlyDictionary TextureQualityLevels => new Dictionary - { - { TextureQuality.Default, null }, - { TextureQuality.Level0, "0" }, - { TextureQuality.Level1, "1" }, - { TextureQuality.Level2, "2" }, - { TextureQuality.Level3, "3" }, - }; - - // 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> - //{ - // { - // InGameMenuVersion.Default, - // new Dictionary - // { - // { "V2Rollout", null }, - // { "EnableV4", null }, - // { "EnableV4Chrome", null }, - // { "ABTest", null }, - // { "ReportButtonCutOff", null } - // } - // }, - - // { - // InGameMenuVersion.V1, - // new Dictionary - // { - // { "V2Rollout", "0" }, - // { "EnableV4", "False" }, - // { "EnableV4Chrome", "False" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", "False" } - // } - // }, - - // { - // InGameMenuVersion.V2, - // new Dictionary - // { - // { "V2Rollout", "100" }, - // { "EnableV4", "False" }, - // { "EnableV4Chrome", "False" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", null } - // } - // }, - - // { - // InGameMenuVersion.V4, - // new Dictionary - // { - // { "V2Rollout", "0" }, - // { "EnableV4", "True" }, - // { "EnableV4Chrome", "False" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", null } - // } - // }, - - // { - // InGameMenuVersion.V4Chrome, - // new Dictionary - // { - // { "V2Rollout", "0" }, - // { "EnableV4", "True" }, - // { "EnableV4Chrome", "True" }, - // { "ABTest", "False" }, - // { "ReportButtonCutOff", null } - // } - // } - //}; - - // all fflags are stored as strings - // 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(LOG_IDENT, $"Deletion of '{key}' is pending"); - - Prop.Remove(key); - } - else - { - if (Prop.ContainsKey(key)) - { - if (key == Prop[key].ToString()) - return; - - App.Logger.WriteLine(LOG_IDENT, $"Changing of '{key}' from '{Prop[key]}' to '{value}' is pending"); - } - else - { - App.Logger.WriteLine(LOG_IDENT, $"Setting of '{key}' to '{value}' is pending"); - } - - Prop[key] = value.ToString()!; - } - } - - // this returns null if the fflag doesn't exist - public string? GetValue(string key) - { - // check if we have an updated change for it pushed first - if (Prop.TryGetValue(key, out object? value) && value is not null) - return value.ToString(); - - return null; - } - - public void SetPreset(string prefix, object? value) - { - foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix))) - SetValue(pair.Value, value); - } - - public void SetPresetEnum(string prefix, string target, object? value) - { - foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix))) - { - if (pair.Key.StartsWith($"{prefix}.{target}")) - SetValue(pair.Value, value); - else - SetValue(pair.Value, null); - } - } - - public string? GetPreset(string name) - { - if (!PresetFlags.ContainsKey(name)) - { - App.Logger.WriteLine("FastFlagManager::GetPreset", $"Could not find preset {name}"); - Debug.Assert(false, $"Could not find preset {name}"); - return null; - } - - return GetValue(PresetFlags[name]); - } - - public T GetPresetEnum(IReadOnlyDictionary mapping, string prefix, string value) where T : Enum - { - foreach (var pair in mapping) - { - if (pair.Value == "None") - continue; - - if (GetPreset($"{prefix}.{pair.Value}") == value) - return pair.Key; - } - - return mapping.First().Key; - } - - public override void Save() - { - // convert all flag values to strings before saving - - foreach (var pair in Prop) - Prop[pair.Key] = pair.Value.ToString()!; - - base.Save(); - - // clone the dictionary - OriginalProp = new(Prop); - } - - public override void Load(bool alertFailure = true) - { - base.Load(alertFailure); - - // clone the dictionary - OriginalProp = new(Prop); - - if (GetPreset("Network.Log") != "7") - SetPreset("Network.Log", "7"); - - if (GetPreset("Rendering.ManualFullscreen") != "False") - SetPreset("Rendering.ManualFullscreen", "False"); - } - } -} diff --git a/Bloxstrap/GlobalCache.cs b/Bloxstrap/GlobalCache.cs deleted file mode 100644 index 6977224..0000000 --- a/Bloxstrap/GlobalCache.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bloxstrap -{ - public static class GlobalCache - { - public static readonly Dictionary ServerLocation = new(); - } -} diff --git a/Bloxstrap/GlobalUsings.cs b/Bloxstrap/GlobalUsings.cs deleted file mode 100644 index c04aec2..0000000 --- a/Bloxstrap/GlobalUsings.cs +++ /dev/null @@ -1,32 +0,0 @@ -global using System; -global using System.Collections.Generic; -global using System.Diagnostics; -global using System.Globalization; -global using System.IO; -global using System.Text; -global using System.Text.Json; -global using System.Text.Json.Serialization; -global using System.Text.RegularExpressions; -global using System.Linq; -global using System.Net; -global using System.Net.Http; -global using System.Threading; -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.APIs.Config; -global using Bloxstrap.Models.APIs.GitHub; -global using Bloxstrap.Models.APIs.Roblox; -global using Bloxstrap.Models.Attributes; -global using Bloxstrap.Models.BloxstrapRPC; -global using Bloxstrap.Models.Entities; -global using Bloxstrap.Models.Manifest; -global using Bloxstrap.Models.Persistable; -global using Bloxstrap.Models.SettingTasks; -global using Bloxstrap.Models.SettingTasks.Base; -global using Bloxstrap.Resources; -global using Bloxstrap.UI; -global using Bloxstrap.Utility; \ No newline at end of file diff --git a/Bloxstrap/HttpClientLoggingHandler.cs b/Bloxstrap/HttpClientLoggingHandler.cs deleted file mode 100644 index 3eb830b..0000000 --- a/Bloxstrap/HttpClientLoggingHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Bloxstrap -{ - internal class HttpClientLoggingHandler : MessageProcessingHandler - { - public HttpClientLoggingHandler(HttpMessageHandler innerHandler) - : base(innerHandler) - { - } - - protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) - { - App.Logger.WriteLine("HttpClientLoggingHandler::ProcessRequest", $"{request.Method} {request.RequestUri}"); - return request; - } - - protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken) - { - App.Logger.WriteLine("HttpClientLoggingHandler::ProcessResponse", $"{(int)response.StatusCode} {response.ReasonPhrase} {response.RequestMessage!.RequestUri}"); - return response; - } - } -} diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs deleted file mode 100644 index 93f40c1..0000000 --- a/Bloxstrap/Installer.cs +++ /dev/null @@ -1,624 +0,0 @@ -using System.Windows; -using Microsoft.Win32; - -namespace Bloxstrap -{ - internal class Installer - { - /// - /// Should this version automatically open the release notes page? - /// Recommended for major updates only. - /// - private const bool OpenReleaseNotes = false; - - private static string DesktopShortcut => Path.Combine(Paths.Desktop, $"{App.ProjectName}.lnk"); - - private static string StartMenuShortcut => Path.Combine(Paths.WindowsStartMenu, $"{App.ProjectName}.lnk"); - - public string InstallLocation = Path.Combine(Paths.LocalAppData, App.ProjectName); - - public bool ExistingDataPresent => File.Exists(Path.Combine(InstallLocation, "Settings.json")); - - public bool CreateDesktopShortcuts = true; - - public bool CreateStartMenuShortcuts = true; - - public bool EnableAnalytics = true; - - public bool IsImplicitInstall = false; - - public string InstallLocationError { get; set; } = ""; - - public void DoInstall() - { - const string LOG_IDENT = "Installer::DoInstall"; - - App.Logger.WriteLine(LOG_IDENT, "Beginning installation"); - - // should've been created earlier from the write test anyway - Directory.CreateDirectory(InstallLocation); - - Paths.Initialize(InstallLocation); - - if (!IsImplicitInstall) - { - Filesystem.AssertReadOnly(Paths.Application); - - try - { - File.Copy(Paths.Process, Paths.Application, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Could not overwrite executable"); - App.Logger.WriteException(LOG_IDENT, ex); - - Frontend.ShowMessageBox(Strings.Installer_Install_CannotOverwrite, MessageBoxImage.Error); - App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); - } - } - - using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) - { - uninstallKey.SetValueSafe("DisplayIcon", $"{Paths.Application},0"); - uninstallKey.SetValueSafe("DisplayName", App.ProjectName); - - uninstallKey.SetValueSafe("DisplayVersion", App.Version); - - if (uninstallKey.GetValue("InstallDate") is null) - uninstallKey.SetValueSafe("InstallDate", DateTime.Now.ToString("yyyyMMdd")); - - uninstallKey.SetValueSafe("InstallLocation", Paths.Base); - uninstallKey.SetValueSafe("NoRepair", 1); - uninstallKey.SetValueSafe("Publisher", App.ProjectOwner); - uninstallKey.SetValueSafe("ModifyPath", $"\"{Paths.Application}\" -settings"); - uninstallKey.SetValueSafe("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); - uninstallKey.SetValueSafe("UninstallString", $"\"{Paths.Application}\" -uninstall"); - uninstallKey.SetValueSafe("HelpLink", App.ProjectHelpLink); - uninstallKey.SetValueSafe("URLInfoAbout", App.ProjectSupportLink); - uninstallKey.SetValueSafe("URLUpdateInfo", App.ProjectDownloadLink); - } - - // only register player, for the scenario where the user installs bloxstrap, closes it, - // and then launches from the website expecting it to work - // studio can be implicitly registered when it's first launched manually - WindowsRegistry.RegisterPlayer(); - - if (CreateDesktopShortcuts) - Shortcut.Create(Paths.Application, "", DesktopShortcut); - - if (CreateStartMenuShortcuts) - Shortcut.Create(Paths.Application, "", StartMenuShortcut); - - // existing configuration persisting from an earlier install - App.Settings.Load(false); - App.State.Load(false); - App.FastFlags.Load(false); - - App.Settings.Prop.EnableAnalytics = EnableAnalytics; - - if (App.IsStudioVisible) - WindowsRegistry.RegisterStudio(); - - App.Settings.Save(); - - App.Logger.WriteLine(LOG_IDENT, "Installation finished"); - - if (!IsImplicitInstall) - App.SendStat("installAction", "install"); - } - - private bool ValidateLocation() - { - // prevent from installing to the root of a drive - if (InstallLocation.Length <= 3) - return false; - - // unc path, just to be safe - if (InstallLocation.StartsWith("\\\\")) - return false; - - if (InstallLocation.StartsWith(Path.GetTempPath(), StringComparison.InvariantCultureIgnoreCase) - || InstallLocation.Contains("\\Temp\\", StringComparison.InvariantCultureIgnoreCase)) - return false; - - // prevent from installing to a onedrive folder - if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase)) - return false; - - // prevent from installing to an essential user profile folder (e.g. Documents, Downloads, Contacts idk) - if (String.Compare(Directory.GetParent(InstallLocation)?.FullName, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase) == 0) - return false; - - // prevent from installing into the program files folder - if (InstallLocation.Contains("Program Files")) - return false; - - return true; - } - - public bool CheckInstallLocation() - { - if (string.IsNullOrEmpty(InstallLocation)) - { - InstallLocationError = Strings.Menu_InstallLocation_NotSet; - } - else if (!ValidateLocation()) - { - InstallLocationError = Strings.Menu_InstallLocation_CantInstall; - } - else - { - if (!IsImplicitInstall - && !InstallLocation.EndsWith(App.ProjectName, StringComparison.InvariantCultureIgnoreCase) - && Directory.Exists(InstallLocation) - && Directory.EnumerateFileSystemEntries(InstallLocation).Any()) - { - string suggestedChange = Path.Combine(InstallLocation, App.ProjectName); - - MessageBoxResult result = Frontend.ShowMessageBox( - String.Format(Strings.Menu_InstallLocation_NotEmpty, suggestedChange), - MessageBoxImage.Warning, - MessageBoxButton.YesNoCancel, - MessageBoxResult.Yes - ); - - if (result == MessageBoxResult.Yes) - InstallLocation = suggestedChange; - else if (result == MessageBoxResult.Cancel || result == MessageBoxResult.None) - return false; - } - - try - { - // check if we can write to the directory (a bit hacky but eh) - string testFile = Path.Combine(InstallLocation, $"{App.ProjectName}WriteTest.txt"); - - Directory.CreateDirectory(InstallLocation); - File.WriteAllText(testFile, ""); - File.Delete(testFile); - } - catch (UnauthorizedAccessException) - { - InstallLocationError = Strings.Menu_InstallLocation_NoWritePerms; - } - catch (Exception ex) - { - InstallLocationError = ex.Message; - } - } - - return String.IsNullOrEmpty(InstallLocationError); - } - - public static void DoUninstall(bool keepData) - { - const string LOG_IDENT = "Installer::DoUninstall"; - - var processes = new List(); - - if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid)) - processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName)); - - if (App.IsStudioVisible) - processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName)); - - // prompt to shutdown roblox if its currently running - if (processes.Any()) - { - var result = Frontend.ShowMessageBox( - Strings.Bootstrapper_Uninstall_RobloxRunning, - MessageBoxImage.Information, - MessageBoxButton.OKCancel, - MessageBoxResult.OK - ); - - if (result != MessageBoxResult.OK) - { - App.Terminate(ErrorCode.ERROR_CANCELLED); - return; - } - - try - { - foreach (var process in processes) - { - process.Kill(); - process.Close(); - } - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}"); - } - } - - string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox"); - bool playerStillInstalled = true; - bool studioStillInstalled = true; - - // check if stock bootstrapper is still installed - using var playerKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); - var playerFolder = playerKey?.GetValue("InstallLocation"); - - if (playerKey is null || playerFolder is not string) - { - playerStillInstalled = false; - - WindowsRegistry.Unregister("roblox"); - WindowsRegistry.Unregister("roblox-player"); - } - else - { - string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe"); - - WindowsRegistry.RegisterPlayer(playerPath, "%1"); - } - - using var studioKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); - var studioFolder = studioKey?.GetValue("InstallLocation"); - - if (studioKey is null || studioFolder is not string) - { - studioStillInstalled = false; - - WindowsRegistry.Unregister("roblox-studio"); - WindowsRegistry.Unregister("roblox-studio-auth"); - - WindowsRegistry.Unregister("Roblox.Place"); - WindowsRegistry.Unregister(".rbxl"); - WindowsRegistry.Unregister(".rbxlx"); - } - else - { - string studioPath = Path.Combine((string)studioFolder, "RobloxStudioBeta.exe"); - string studioLauncherPath = Path.Combine((string)studioFolder, "RobloxStudioLauncherBeta.exe"); - - WindowsRegistry.RegisterStudioProtocol(studioPath, "%1"); - WindowsRegistry.RegisterStudioFileClass(studioPath, "-ide \"%1\""); - } - - var cleanupSequence = new List - { - () => - { - foreach (var file in Directory.GetFiles(Paths.Desktop).Where(x => x.EndsWith("lnk"))) - { - var shortcut = ShellLink.Shortcut.ReadFromFile(file); - - if (shortcut.ExtraData.EnvironmentVariableDataBlock?.TargetUnicode == Paths.Application) - File.Delete(file); - } - }, - - () => File.Delete(StartMenuShortcut), - - () => Directory.Delete(Paths.Versions, true), - () => Directory.Delete(Paths.Downloads, true), - - () => File.Delete(App.State.FileLocation) - }; - - if (!keepData) - { - cleanupSequence.AddRange(new List - { - () => Directory.Delete(Paths.Modifications, true), - () => Directory.Delete(Paths.Logs, true), - - () => File.Delete(App.Settings.FileLocation) - }); - } - - bool deleteFolder = Directory.GetFiles(Paths.Base).Length <= 3; - - if (deleteFolder) - cleanupSequence.Add(() => Directory.Delete(Paths.Base, true)); - - if (!playerStillInstalled && !studioStillInstalled && Directory.Exists(robloxFolder)) - cleanupSequence.Add(() => Directory.Delete(robloxFolder, true)); - - cleanupSequence.Add(() => Registry.CurrentUser.DeleteSubKey(App.UninstallKey)); - - foreach (var process in cleanupSequence) - { - try - { - process(); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - - if (Directory.Exists(Paths.Base)) - { - // this is definitely one of the workaround hacks of all time - - string deleteCommand; - - if (deleteFolder) - deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\""; - else - deleteCommand = $"del /Q \"{Paths.Application}\""; - - Process.Start(new ProcessStartInfo() - { - FileName = "cmd.exe", - Arguments = $"/c timeout 5 && {deleteCommand}", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - } - - App.SendStat("installAction", "uninstall"); - } - - public static void HandleUpgrade() - { - const string LOG_IDENT = "Installer::HandleUpgrade"; - - if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application) - return; - - // 2.0.0 downloads updates to /Updates so lol - bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active - || Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) - || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")) - || Paths.Process.StartsWith(Paths.TempUpdates); - - var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; - var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion; - - if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application)) - return; - - if (currentVer is not null && existingVer is not null && Utilities.CompareVersions(currentVer, existingVer) == VersionComparison.LessThan) - { - var result = Frontend.ShowMessageBox( - Strings.InstallChecker_VersionLessThanInstalled, - MessageBoxImage.Question, - MessageBoxButton.YesNo - ); - - if (result != MessageBoxResult.Yes) - return; - } - - // silently upgrade version if the command line flag is set or if we're launching from an auto update - if (!isAutoUpgrade) - { - var result = Frontend.ShowMessageBox( - Strings.InstallChecker_VersionDifferentThanInstalled, - MessageBoxImage.Question, - MessageBoxButton.YesNo - ); - - if (result != MessageBoxResult.Yes) - return; - } - - App.Logger.WriteLine(LOG_IDENT, "Doing upgrade"); - - Filesystem.AssertReadOnly(Paths.Application); - - using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5))) - { - if (!ipl.IsAcquired) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)"); - return; - } - } - - // prior to 2.8.0, auto-updating was handled with this... bruteforce method - // now it's handled with the system mutex you see above, but we need to keep this logic for <2.8.0 versions - for (int i = 1; i <= 10; i++) - { - try - { - File.Copy(Paths.Process, Paths.Application, true); - break; - } - catch (Exception ex) - { - if (i == 1) - { - App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); - } - else if (i == 10) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 10 tries/5 seconds)"); - App.Logger.WriteException(LOG_IDENT, ex); - return; - } - - Thread.Sleep(500); - } - } - - using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) - { - uninstallKey.SetValueSafe("DisplayVersion", App.Version); - - uninstallKey.SetValueSafe("Publisher", App.ProjectOwner); - uninstallKey.SetValueSafe("HelpLink", App.ProjectHelpLink); - uninstallKey.SetValueSafe("URLInfoAbout", App.ProjectSupportLink); - uninstallKey.SetValueSafe("URLUpdateInfo", App.ProjectDownloadLink); - } - - // update migrations - - if (existingVer is not null) - { - if (Utilities.CompareVersions(existingVer, "2.2.0") == VersionComparison.LessThan) - { - string path = Path.Combine(Paths.Integrations, "rbxfpsunlocker"); - - try - { - if (Directory.Exists(path)) - Directory.Delete(path, true); - } - catch (Exception ex) - { - App.Logger.WriteException(LOG_IDENT, ex); - } - } - - if (Utilities.CompareVersions(existingVer, "2.3.0") == VersionComparison.LessThan) - { - string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll"); - string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini"); - - if (File.Exists(injectorLocation)) - File.Delete(injectorLocation); - - if (File.Exists(configLocation)) - File.Delete(configLocation); - } - - - if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan) - { - App.FastFlags.SetValue("DFFlagDisableDPIScale", null); - App.FastFlags.SetValue("DFFlagVariableDPIScale2", null); - } - - if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan) - { - if (App.Settings.Prop.UseDisableAppPatch) - { - try - { - File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl")); - } - catch (Exception ex) - { - App.Logger.WriteException(LOG_IDENT, ex); - } - - App.Settings.Prop.EnableActivityTracking = true; - } - - if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog) - App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog; - - _ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x); - if (x == 0) - App.FastFlags.SetPreset("Rendering.Framerate", null); - } - - if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan) - { - if (isAutoUpgrade) - { - if (App.LaunchSettings.Args.Length == 0) - App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player; - - string? query = App.LaunchSettings.Args.FirstOrDefault(x => x.Contains("roblox")); - - if (query is not null) - { - App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player; - App.LaunchSettings.RobloxLaunchArgs = query; - } - } - - string oldDesktopPath = Path.Combine(Paths.Desktop, "Play Roblox.lnk"); - string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap"); - - if (File.Exists(oldDesktopPath)) - File.Move(oldDesktopPath, DesktopShortcut, true); - - if (Directory.Exists(oldStartPath)) - { - try - { - Directory.Delete(oldStartPath, true); - } - catch (Exception ex) - { - App.Logger.WriteException(LOG_IDENT, ex); - } - - Shortcut.Create(Paths.Application, "", StartMenuShortcut); - } - - Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false); - - WindowsRegistry.RegisterPlayer(); - - App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null); - App.FastFlags.SetValue("FFlagFixGraphicsQuality", null); - } - - if (Utilities.CompareVersions(existingVer, "2.8.1") == VersionComparison.LessThan) - { - // wipe all escape menu flag presets - App.FastFlags.SetValue("FIntNewInGameMenuPercentRollout3", null); - App.FastFlags.SetValue("FFlagEnableInGameMenuControls", null); - App.FastFlags.SetValue("FFlagEnableInGameMenuModernization", null); - App.FastFlags.SetValue("FFlagEnableInGameMenuChrome", null); - App.FastFlags.SetValue("FFlagFixReportButtonCutOff", null); - App.FastFlags.SetValue("FFlagEnableMenuControlsABTest", null); - App.FastFlags.SetValue("FFlagEnableV3MenuABTest3", null); - App.FastFlags.SetValue("FFlagEnableInGameMenuChromeABTest3", null); - App.FastFlags.SetValue("FFlagEnableInGameMenuChromeABTest4", null); - } - - if (Utilities.CompareVersions(existingVer, "2.8.2") == VersionComparison.LessThan) - { - string robloxDirectory = Path.Combine(Paths.Base, "Roblox"); - - if (Directory.Exists(robloxDirectory)) - { - try - { - Directory.Delete(robloxDirectory, true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to delete the Roblox directory"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - } - - if (Utilities.CompareVersions(existingVer, "2.8.3") == VersionComparison.LessThan) - { - // force reinstallation - App.State.Prop.Player.VersionGuid = ""; - App.State.Prop.Studio.VersionGuid = ""; - } - - App.Settings.Save(); - App.FastFlags.Save(); - App.State.Save(); - } - - if (currentVer is null) - return; - - App.SendStat("installAction", "upgrade"); - - if (isAutoUpgrade) - { -#pragma warning disable CS0162 // Unreachable code detected - if (OpenReleaseNotes) - Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}"); -#pragma warning restore CS0162 // Unreachable code detected - } - else - { - Frontend.ShowMessageBox( - string.Format(Strings.InstallChecker_Updated, currentVer), - MessageBoxImage.Information, - MessageBoxButton.OK - ); - } - } - } -} diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs deleted file mode 100644 index 8132250..0000000 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ /dev/null @@ -1,389 +0,0 @@ -namespace Bloxstrap.Integrations -{ - public class ActivityWatcher : IDisposable - { - private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]"; - private const string GameJoiningEntry = "[FLog::Output] ! Joining game"; - - // these entries are technically volatile! - // they only get printed depending on their configured FLog level, which could change at any time - // while levels being changed is fairly rare, please limit the number of varying number of FLog types you have to use, if possible - - private const string GameTeleportingEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToPlace"; - private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer"; - private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer"; - private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:"; - private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = "; - private const string GameJoinedEntry = "[FLog::Network] serverId:"; - private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:"; - private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal"; - - private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; - private const string GameJoiningPrivateServerPattern = @"""accessCode"":""([0-9a-f\-]{36})"""; - private const string GameJoiningUniversePattern = @"universeid:([0-9]+).*userid:([0-9]+)"; - private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+"; - private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+"; - private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)"; - - private int _logEntriesRead = 0; - private bool _teleportMarker = false; - private bool _reservedTeleportMarker = false; - - public event EventHandler? OnLogEntry; - public event EventHandler? OnGameJoin; - public event EventHandler? OnGameLeave; - public event EventHandler? OnLogOpen; - public event EventHandler? OnAppClose; - public event EventHandler? OnRPCMessage; - - private DateTime LastRPCRequest; - - public string LogLocation = null!; - - public bool InGame = false; - - public ActivityData Data { get; private set; } = new(); - - /// - /// Ordered by newest to oldest - /// - public List History = new(); - - public bool IsDisposed = false; - - public ActivityWatcher(string? logFile = null) - { - if (!String.IsNullOrEmpty(logFile)) - LogLocation = logFile; - } - - public async void Start() - { - const string LOG_IDENT = "ActivityWatcher::Start"; - - // okay, here's the process: - // - // - tail the latest log file from %localappdata%\roblox\logs - // - check for specific lines to determine player's game activity as shown below: - // - // - get the place id, job id and machine address from '! Joining game '{{JOBID}}' place {{PLACEID}} at {{MACHINEADDRESS}}' entry - // - confirm place join with 'serverId: {{MACHINEADDRESS}}|{{MACHINEPORT}}' entry - // - check for leaves/disconnects with 'Time to disconnect replication data: {{TIME}}' entry - // - // we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity - - FileInfo logFileInfo; - - if (String.IsNullOrEmpty(LogLocation)) - { - string logDirectory = Path.Combine(Paths.LocalAppData, "Roblox\\logs"); - - if (!Directory.Exists(logDirectory)) - return; - - // we need to make sure we're fetching the absolute latest log file - // 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(LOG_IDENT, "Opening Roblox log file..."); - - while (true) - { - logFileInfo = new DirectoryInfo(logDirectory) - .GetFiles() - .Where(x => x.Name.Contains("Player", StringComparison.OrdinalIgnoreCase) && x.CreationTime <= DateTime.Now) - .OrderByDescending(x => x.CreationTime) - .First(); - - if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now) - break; - - App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})"); - await Task.Delay(1000); - } - - LogLocation = logFileInfo.FullName; - } - else - { - logFileInfo = new FileInfo(LogLocation); - } - - OnLogOpen?.Invoke(this, EventArgs.Empty); - - var logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}"); - - using var streamReader = new StreamReader(logFileStream); - - while (!IsDisposed) - { - string? log = await streamReader.ReadLineAsync(); - - if (log is null) - await Task.Delay(1000); - else - ReadLogEntry(log); - } - } - - private void ReadLogEntry(string entry) - { - const string LOG_IDENT = "ActivityWatcher::ReadLogEntry"; - - OnLogEntry?.Invoke(this, entry); - - _logEntriesRead += 1; - - // 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(LOG_IDENT, $"Read {_logEntriesRead} log entries"); - else if (_logEntriesRead % 100 == 0) - App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries"); - - if (entry.Contains(GameLeavingEntry)) - { - App.Logger.WriteLine(LOG_IDENT, "User is back into the desktop app"); - - OnAppClose?.Invoke(this, EventArgs.Empty); - - if (Data.PlaceId != 0 && !InGame) - { - App.Logger.WriteLine(LOG_IDENT, "User appears to be leaving from a cancelled/errored join"); - Data = new(); - } - } - - if (!InGame && Data.PlaceId == 0) - { - // We are not in a game, nor are in the process of joining one - - if (entry.Contains(GameJoiningPrivateServerEntry)) - { - // we only expect to be joining a private server if we're not already in a game - - Data.ServerType = ServerType.Private; - - var match = Regex.Match(entry, GameJoiningPrivateServerPattern); - - if (match.Groups.Count != 2) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join private server entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - Data.AccessCode = match.Groups[1].Value; - } - else if (entry.Contains(GameJoiningEntry)) - { - Match match = Regex.Match(entry, GameJoiningEntryPattern); - - if (match.Groups.Count != 4) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - InGame = false; - Data.PlaceId = long.Parse(match.Groups[2].Value); - Data.JobId = match.Groups[1].Value; - Data.MachineAddress = match.Groups[3].Value; - - if (App.Settings.Prop.ShowServerDetails && Data.MachineAddressValid) - _ = Data.QueryServerLocation(); - - if (_teleportMarker) - { - Data.IsTeleport = true; - _teleportMarker = false; - } - - if (_reservedTeleportMarker) - { - Data.ServerType = ServerType.Reserved; - _reservedTeleportMarker = false; - } - - App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({Data})"); - } - } - else if (!InGame && Data.PlaceId != 0) - { - // We are not confirmed to be in a game, but we are in the process of joining one - - if (entry.Contains(GameJoiningUniverseEntry)) - { - var match = Regex.Match(entry, GameJoiningUniversePattern); - - if (match.Groups.Count != 3) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join universe entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - Data.UniverseId = Int64.Parse(match.Groups[1].Value); - Data.UserId = Int64.Parse(match.Groups[2].Value); - - if (History.Any()) - { - var lastActivity = History.First(); - - if (Data.UniverseId == lastActivity.UniverseId && Data.IsTeleport) - Data.RootActivity = lastActivity.RootActivity ?? lastActivity; - } - } - else if (entry.Contains(GameJoiningUDMUXEntry)) - { - var match = Regex.Match(entry, GameJoiningUDMUXPattern); - - if (match.Groups.Count != 3 || match.Groups[2].Value != Data.MachineAddress) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - Data.MachineAddress = match.Groups[1].Value; - - if (App.Settings.Prop.ShowServerDetails) - _ = Data.QueryServerLocation(); - - App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({Data})"); - } - else if (entry.Contains(GameJoinedEntry)) - { - Match match = Regex.Match(entry, GameJoinedEntryPattern); - - if (match.Groups.Count != 2 || match.Groups[1].Value != Data.MachineAddress) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({Data})"); - - InGame = true; - Data.TimeJoined = DateTime.Now; - - OnGameJoin?.Invoke(this, EventArgs.Empty); - } - } - else if (InGame && Data.PlaceId != 0) - { - // We are confirmed to be in a game - - if (entry.Contains(GameDisconnectedEntry)) - { - App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({Data})"); - - Data.TimeLeft = DateTime.Now; - History.Insert(0, Data); - - InGame = false; - Data = new(); - - OnGameLeave?.Invoke(this, EventArgs.Empty); - } - else if (entry.Contains(GameTeleportingEntry)) - { - App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({Data})"); - _teleportMarker = true; - } - else if (entry.Contains(GameJoiningReservedServerEntry)) - { - _teleportMarker = true; - _reservedTeleportMarker = true; - } - else if (entry.Contains(GameMessageEntry)) - { - var match = Regex.Match(entry, GameMessageEntryPattern); - - if (match.Groups.Count != 2) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for RPC message entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - string messagePlain = match.Groups[1].Value; - Message? message; - - 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); - } - catch (Exception) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)"); - return; - } - - if (message is null) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)"); - return; - } - - if (string.IsNullOrEmpty(message.Command)) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (Command is empty)"); - return; - } - - if (message.Command == "SetLaunchData") - { - string? data; - - try - { - data = message.Data.Deserialize(); - } - catch (Exception) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)"); - return; - } - - if (data is null) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)"); - return; - } - - if (data.Length > 200) - { - App.Logger.WriteLine(LOG_IDENT, "Data cannot be longer than 200 characters"); - return; - } - - Data.RPCLaunchData = data; - } - - OnRPCMessage?.Invoke(this, message); - - LastRPCRequest = DateTime.Now; - } - } - } - - public void Dispose() - { - IsDisposed = true; - GC.SuppressFinalize(this); - } - } -} diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs deleted file mode 100644 index 8d140f9..0000000 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System.Windows; -using Bloxstrap.Models.RobloxApi; -using DiscordRPC; - -namespace Bloxstrap.Integrations -{ - public class DiscordRichPresence : IDisposable - { - private readonly DiscordRpcClient _rpcClient = new("1005469189907173486"); - private readonly ActivityWatcher _activityWatcher; - private readonly Queue _messageQueue = new(); - - private DiscordRPC.RichPresence? _currentPresence; - private DiscordRPC.RichPresence? _originalPresence; - - private bool _visible = true; - - public DiscordRichPresence(ActivityWatcher activityWatcher) - { - const string LOG_IDENT = "DiscordRichPresence"; - - _activityWatcher = activityWatcher; - - _activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame()); - _activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame()); - _activityWatcher.OnRPCMessage += (_, message) => ProcessRPCMessage(message); - - _rpcClient.OnReady += (_, e) => - App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})"); - - _rpcClient.OnPresenceUpdate += (_, e) => - App.Logger.WriteLine(LOG_IDENT, "Presence updated"); - - _rpcClient.OnError += (_, e) => - App.Logger.WriteLine(LOG_IDENT, $"An RPC error occurred - {e.Message}"); - - _rpcClient.OnConnectionEstablished += (_, e) => - 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(LOG_IDENT, "Failed to establish connection with Discord RPC"); - - _rpcClient.OnClose += (_, e) => - App.Logger.WriteLine(LOG_IDENT, $"Lost connection to Discord RPC - {e.Reason} ({e.Code})"); - - _rpcClient.Initialize(); - } - - public void ProcessRPCMessage(Message message, bool implicitUpdate = true) - { - const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage"; - - if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData") - return; - - if (_currentPresence is null || _originalPresence is null) - { - App.Logger.WriteLine(LOG_IDENT, "Presence is not set, enqueuing message"); - _messageQueue.Enqueue(message); - return; - } - - // a lot of repeated code here, could this somehow be cleaned up? - - if (message.Command == "SetLaunchData") - { - _currentPresence.Buttons = GetButtons(); - } - else if (message.Command == "SetRichPresence") - { - Models.BloxstrapRPC.RichPresence? presenceData; - - try - { - presenceData = message.Data.Deserialize(); - } - catch (Exception) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)"); - return; - } - - if (presenceData is null) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)"); - return; - } - - 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 = _originalPresence.Details; - else - _currentPresence.Details = presenceData.Details; - } - - if (presenceData.State is not null) - { - if (presenceData.State.Length > 128) - App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters"); - else if (presenceData.State == "") - _currentPresence.State = _originalPresence.State; - else - _currentPresence.State = presenceData.State; - } - - 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) - { - if (presenceData.SmallImage.Clear) - { - _currentPresence.Assets.SmallImageKey = ""; - } - else if (presenceData.SmallImage.Reset) - { - _currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText; - _currentPresence.Assets.SmallImageKey = _originalPresence.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 (presenceData.LargeImage is not null) - { - if (presenceData.LargeImage.Clear) - { - _currentPresence.Assets.LargeImageKey = ""; - } - else if (presenceData.LargeImage.Reset) - { - _currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText; - _currentPresence.Assets.LargeImageKey = _originalPresence.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; - } - } - } - - if (implicitUpdate) - UpdatePresence(); - } - - public void SetVisibility(bool visible) - { - App.Logger.WriteLine("DiscordRichPresence::SetVisibility", $"Setting presence visibility ({visible})"); - - _visible = visible; - - if (_visible) - UpdatePresence(); - else - _rpcClient.ClearPresence(); - } - - public async Task SetCurrentGame() - { - const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame"; - - if (!_activityWatcher.InGame) - { - App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence"); - - _currentPresence = _originalPresence = null; - _messageQueue.Clear(); - - UpdatePresence(); - return true; - } - - string icon = "roblox"; - string smallImageText = "Roblox"; - string smallImage = "roblox"; - - - var activity = _activityWatcher.Data; - long placeId = activity.PlaceId; - - App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}"); - - // preserve time spent playing if we're teleporting between places in the same universe - var timeStarted = activity.TimeJoined; - - if (activity.RootActivity is not null) - timeStarted = activity.RootActivity.TimeJoined; - - if (activity.UniverseDetails is null) - { - try - { - await UniverseDetails.FetchSingle(activity.UniverseId); - } - catch (Exception ex) - { - App.Logger.WriteException(LOG_IDENT, ex); - Frontend.ShowMessageBox($"{Strings.ActivityWatcher_RichPresenceLoadFailed}\n\n{ex.Message}", MessageBoxImage.Warning); - return false; - } - - activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId); - } - - var universeDetails = activity.UniverseDetails!; - - icon = universeDetails.Thumbnail.ImageUrl; - - if (App.Settings.Prop.ShowAccountOnRichPresence) - { - var userDetails = await UserDetails.Fetch(activity.UserId); - - smallImage = userDetails.Thumbnail.ImageUrl; - smallImageText = $"Playing on {userDetails.Data.DisplayName} (@{userDetails.Data.Name})"; // i.e. "axell (@Axelan_se)" - } - - if (!_activityWatcher.InGame || placeId != activity.PlaceId) - { - App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed"); - return false; - } - - string status = _activityWatcher.Data.ServerType switch - { - ServerType.Private => "In a private server", - ServerType.Reserved => "In a reserved server", - _ => $"by {universeDetails.Data.Creator.Name}" + (universeDetails.Data.Creator.HasVerifiedBadge ? " ☑️" : ""), - }; - - string universeName = universeDetails.Data.Name; - - if (universeName.Length < 2) - universeName = $"{universeName}\x2800\x2800\x2800"; - - _currentPresence = new DiscordRPC.RichPresence - { - Details = universeName, - State = status, - Timestamps = new Timestamps { Start = timeStarted.ToUniversalTime() }, - Buttons = GetButtons(), - Assets = new Assets - { - LargeImageKey = icon, - LargeImageText = universeDetails.Data.Name, - SmallImageKey = smallImage, - SmallImageText = smallImageText - } - }; - - // this is used for configuration from BloxstrapRPC - _originalPresence = _currentPresence.Clone(); - - if (_messageQueue.Any()) - { - App.Logger.WriteLine(LOG_IDENT, "Processing queued messages"); - ProcessRPCMessage(_messageQueue.Dequeue(), false); - } - - UpdatePresence(); - - return true; - } - - public Button[] GetButtons() - { - var buttons = new List - - - - - - - - - - - - - - - - - - - - - diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs deleted file mode 100644 index 5afcc43..0000000 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Windows; -using System.ComponentModel; -using System.Windows.Forms; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shell; - -using Bloxstrap.UI.Elements.Bootstrapper.Base; -using Bloxstrap.UI.ViewModels.Bootstrapper; - -namespace Bloxstrap.UI.Elements.Bootstrapper -{ - /// - /// Interaction logic for ByfronDialog.xaml - /// - public partial class ByfronDialog : IBootstrapperDialog - { - private readonly ByfronDialogViewModel _viewModel; - - public Bloxstrap.Bootstrapper? Bootstrapper { get; set; } - - private bool _isClosing; - - #region UI Elements - public string Message - { - get => _viewModel.Message; - set - { - string message = value; - if (message.EndsWith("...")) - message = message[..^3]; - - _viewModel.Message = message; - _viewModel.OnPropertyChanged(nameof(_viewModel.Message)); - } - } - - public ProgressBarStyle ProgressStyle - { - get => _viewModel.ProgressIndeterminate ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous; - set - { - _viewModel.ProgressIndeterminate = (value == ProgressBarStyle.Marquee); - _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressIndeterminate)); - } - } - - public int ProgressMaximum - { - get => _viewModel.ProgressMaximum; - set - { - _viewModel.ProgressMaximum = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressMaximum)); - } - } - - public int ProgressValue - { - get => _viewModel.ProgressValue; - set - { - _viewModel.ProgressValue = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressValue)); - } - } - - public TaskbarItemProgressState TaskbarProgressState - { - get => _viewModel.TaskbarProgressState; - set - { - _viewModel.TaskbarProgressState = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressState)); - } - } - - public double TaskbarProgressValue - { - get => _viewModel.TaskbarProgressValue; - set - { - _viewModel.TaskbarProgressValue = value; - _viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressValue)); - } - } - - public bool CancelEnabled - { - get => _viewModel.CancelEnabled; - set - { - _viewModel.CancelEnabled = value; - - _viewModel.OnPropertyChanged(nameof(_viewModel.CancelEnabled)); - _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); - - _viewModel.OnPropertyChanged(nameof(_viewModel.VersionTextVisibility)); - _viewModel.OnPropertyChanged(nameof(_viewModel.VersionText)); - } - } - #endregion - - public ByfronDialog() - { - string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false); - _viewModel = new ByfronDialogViewModel(this, version); - DataContext = _viewModel; - Title = App.Settings.Prop.BootstrapperTitle; - Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource(); - - if (App.Settings.Prop.Theme.GetFinal() == Theme.Light) - { - // Matching the roblox website light theme as close as possible. - _viewModel.DialogBorder = new Thickness(1); - _viewModel.Background = new SolidColorBrush(Color.FromRgb(242, 244, 245)); - _viewModel.Foreground = new SolidColorBrush(Color.FromRgb(57, 59, 61)); - _viewModel.IconColor = new SolidColorBrush(Color.FromRgb(57, 59, 61)); - _viewModel.ProgressBarBackground = new SolidColorBrush(Color.FromRgb(189, 190, 190)); - _viewModel.ByfronLogoLocation = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoLight.jpg")); - } - - InitializeComponent(); - } - private void Window_Closing(object sender, CancelEventArgs e) - { - if (!_isClosing) - Bootstrapper?.Cancel(); - } - - #region IBootstrapperDialog Methods - // Referencing FluentDialog - public void ShowBootstrapper() => this.ShowDialog(); - - public void CloseBootstrapper() - { - _isClosing = true; - Dispatcher.BeginInvoke(this.Close); - } - - public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback); - #endregion - } -} diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml deleted file mode 100644 index 76cda5e..0000000 --- a/Bloxstrap/UI/Elements/Bootstrapper/ClassicFluentDialog.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -