Merge branch 'main' into feature/improve-studio-support

This commit is contained in:
bluepilledgreat 2024-11-05 21:13:13 +00:00
commit eb9e1341c7
48 changed files with 808 additions and 325 deletions

View File

@ -5,6 +5,10 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
> [!IMPORTANT]
> Do NOT open an issue if you're getting an error saying "`The given key 'redist.zip' was not present in the dictionary.`", or if you're getting stuck on "Configuring Roblox...".
> This problem has been fixed in the latest version of Bloxstrap. You either have auto-updates disabled or are using someone's custom build of an old version.
> Download the latest version [here](https://github.com/pizzaboxer/bloxstrap/releases/latest).
### **Preliminary instructions** ### **Preliminary instructions**
- Before opening an issue, please [check the Wiki first](https://github.com/pizzaboxer/bloxstrap/wiki/) to see if your problem has been addressed there. - Before opening an issue, please [check the Wiki first](https://github.com/pizzaboxer/bloxstrap/wiki/) to see if your problem has been addressed there.
- If it isn't, please confirm which pages that you read that were relevant to your issue. - If it isn't, please confirm which pages that you read that were relevant to your issue.
@ -26,6 +30,14 @@ body:
- label: I am using the latest version of Bloxstrap. - label: I am using the latest version of Bloxstrap.
required: true required: true
- label: I did not answer truthfully to all the above checkboxes. - label: I did not answer truthfully to all the above checkboxes.
- type: input
id: version
attributes:
label: Bloxstrap Version
description: "What version of Bloxstrap are you using? Find it in the 'About' section of the Settings"
placeholder: "v1.0.0"
validations:
required: true
- type: textarea - type: textarea
id: what-happened id: what-happened
attributes: attributes:
@ -39,4 +51,4 @@ body:
label: Bloxstrap Log label: Bloxstrap Log
description: If you're getting a Bloxstrap Exception error, upload your log file here. Otherwise, just leave it empty. description: If you're getting a Bloxstrap Exception error, upload your log file here. Otherwise, just leave it empty.
value: "N/A" value: "N/A"
#render: text render: text

View File

@ -39,23 +39,17 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
# - name: Sign and download artifact - name: Sign and download artifact
# uses: signpath/github-action-submit-signing-request@v1 uses: signpath/github-action-submit-signing-request@v1
# with:
# api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
# organization-id: '107b3de5-057b-42fc-a985-3546e4261775'
# project-slug: 'bloxstrap'
# signing-policy-slug: 'release-signing'
# artifact-configuration-slug: 'github-ci'
# github-artifact-id: '${{ needs.build.outputs.artifact-id }}'
# wait-for-completion: true
# output-artifact-directory: 'release'
- name: Download x64 release artifact
uses: actions/download-artifact@v4
with: with:
name: Bloxstrap (Release) (${{ github.sha }}) api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
path: release organization-id: '107b3de5-057b-42fc-a985-3546e4261775'
project-slug: 'bloxstrap'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: 'github-ci'
github-artifact-id: '${{ needs.build.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'release'
- name: Rename binaries - name: Rename binaries
run: mv release/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}.exe run: mv release/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}.exe
@ -79,7 +73,7 @@ jobs:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '107b3de5-057b-42fc-a985-3546e4261775' organization-id: '107b3de5-057b-42fc-a985-3546e4261775'
project-slug: 'bloxstrap' project-slug: 'bloxstrap'
signing-policy-slug: 'test-signing' signing-policy-slug: 'release-signing'
artifact-configuration-slug: 'github-ci' artifact-configuration-slug: 'github-ci'
github-artifact-id: '${{ needs.build.outputs.artifact-id }}' github-artifact-id: '${{ needs.build.outputs.artifact-id }}'
wait-for-completion: true wait-for-completion: true

View File

@ -1,6 +1,7 @@
using System.Reflection; using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Windows; using System.Windows;
using System.Windows.Shell;
using System.Windows.Threading; using System.Windows.Threading;
using Microsoft.Win32; using Microsoft.Win32;
@ -35,6 +36,8 @@ namespace Bloxstrap
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; 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 IsActionBuild => !String.IsNullOrEmpty(BuildMetadata.CommitRef);
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal); public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
@ -106,6 +109,16 @@ namespace Bloxstrap
_showingExceptionDialog = true; _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); Frontend.ShowExceptionDialog(ex);
Terminate(ErrorCode.ERROR_INSTALL_FAILURE); Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
@ -150,6 +163,24 @@ namespace Bloxstrap
} }
} }
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);
}
}
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
const string LOG_IDENT = "App::OnStartup"; const string LOG_IDENT = "App::OnStartup";
@ -209,7 +240,6 @@ namespace Bloxstrap
else else
{ {
// check if user profile folder has been renamed // check if user profile folder has been renamed
// honestly, i'll be expecting bugs from this
var match = Regex.Match(value, @"^[a-zA-Z]:\\Users\\([^\\]+)", RegexOptions.IgnoreCase); var match = Regex.Match(value, @"^[a-zA-Z]:\\Users\\([^\\]+)", RegexOptions.IgnoreCase);
if (match.Success) if (match.Success)

View File

@ -32,6 +32,7 @@ namespace Bloxstrap.AppData
{ "content-textures3.zip", @"PlatformContent\pc\textures\" }, { "content-textures3.zip", @"PlatformContent\pc\textures\" },
{ "content-terrain.zip", @"PlatformContent\pc\terrain\" }, { "content-terrain.zip", @"PlatformContent\pc\terrain\" },
{ "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" },
{ "content-platform-dictionaries.zip", @"PlatformContent\pc\shared_compression_dictionaries\" },
{ "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" },
{ "extracontent-translations.zip", @"ExtraContent\translations\" }, { "extracontent-translations.zip", @"ExtraContent\translations\" },

View File

@ -10,10 +10,10 @@
string ExecutableName { get; } string ExecutableName { get; }
string StartEvent { get; }
string Directory { get; } string Directory { get; }
string OldDirectory { get; }
string LockFilePath { get; } string LockFilePath { get; }
string ExecutablePath { get; } string ExecutablePath { get; }

View File

@ -16,10 +16,10 @@ namespace Bloxstrap.AppData
public override string ExecutableName => "RobloxPlayerBeta.exe"; public override string ExecutableName => "RobloxPlayerBeta.exe";
public string StartEvent => "www.roblox.com/robloxStartedEvent";
public override string Directory => Path.Combine(Paths.Roblox, "Player"); public override string Directory => Path.Combine(Paths.Roblox, "Player");
public string OldDirectory => Path.Combine(Paths.Roblox, "Player.old");
public AppState State => App.State.Prop.Player; public AppState State => App.State.Prop.Player;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>() public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()

View File

@ -10,10 +10,10 @@
public override string ExecutableName => "RobloxStudioBeta.exe"; public override string ExecutableName => "RobloxStudioBeta.exe";
public string StartEvent => "www.roblox.com/robloxStudioStartedEvent";
public override string Directory => Path.Combine(Paths.Roblox, "Studio"); public override string Directory => Path.Combine(Paths.Roblox, "Studio");
public string OldDirectory => Path.Combine(Paths.Roblox, "Studio.old");
public AppState State => App.State.Prop.Studio; public AppState State => App.State.Prop.Studio;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>() public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()

View File

@ -7,8 +7,8 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms> <UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon> <ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>2.8.0</Version> <Version>2.8.1</Version>
<FileVersion>2.8.0</FileVersion> <FileVersion>2.8.1</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>

View File

@ -11,6 +11,8 @@
#warning "Automatic updater debugging is enabled" #warning "Automatic updater debugging is enabled"
#endif #endif
using System.ComponentModel;
using System.Data;
using System.Windows; using System.Windows;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Shell; using System.Windows.Shell;
@ -116,17 +118,21 @@ namespace Bloxstrap
private void HandleConnectionError(Exception exception) private void HandleConnectionError(Exception exception)
{ {
const string LOG_IDENT = "Bootstrapper::HandleConnectionError";
_noConnection = true; _noConnection = true;
string message = Strings.Dialog_Connectivity_Preventing; App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed");
App.Logger.WriteException(LOG_IDENT, exception);
if (exception.GetType() == typeof(AggregateException)) string message = Strings.Dialog_Connectivity_BadConnection;
if (exception is AggregateException)
exception = exception.InnerException!; exception = exception.InnerException!;
if (exception.GetType() == typeof(HttpRequestException)) // 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)"); message = String.Format(Strings.Dialog_Connectivity_RobloxDown, "[status.roblox.com](https://status.roblox.com)");
else if (exception.GetType() == typeof(TaskCanceledException))
message = Strings.Dialog_Connectivity_TimedOut;
if (_mustUpgrade) if (_mustUpgrade)
message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeNeeded}\n\n{Strings.Dialog_Connectivity_TryAgainLater}"; message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeNeeded}\n\n{Strings.Dialog_Connectivity_TryAgainLater}";
@ -245,6 +251,10 @@ namespace Bloxstrap
Dialog?.CloseBootstrapper(); Dialog?.CloseBootstrapper();
} }
/// <summary>
/// Will throw whatever HttpClient can throw
/// </summary>
/// <returns></returns>
private async Task GetLatestVersionInfo() private async Task GetLatestVersionInfo()
{ {
const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo"; const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo";
@ -255,7 +265,11 @@ namespace Bloxstrap
using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel"); 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); var match = Regex.Match(
App.LaunchSettings.RobloxLaunchArgs,
"channel:([a-zA-Z0-9-_]+)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
);
if (match.Groups.Count == 2) if (match.Groups.Count == 2)
{ {
@ -266,9 +280,12 @@ namespace Bloxstrap
Deployment.Channel = value.ToLowerInvariant(); Deployment.Channel = value.ToLowerInvariant();
} }
App.Logger.WriteLine(LOG_IDENT, "Got channel as " + (String.IsNullOrEmpty(Deployment.Channel) ? Deployment.DefaultChannel : Deployment.Channel)); if (String.IsNullOrEmpty(Deployment.Channel))
Deployment.Channel = Deployment.DefaultChannel;
if (Deployment.Channel != "production") App.Logger.WriteLine(LOG_IDENT, $"Got channel as {Deployment.DefaultChannel}");
if (!Deployment.IsDefaultChannel)
App.SendStat("robloxChannel", Deployment.Channel); App.SendStat("robloxChannel", Deployment.Channel);
ClientVersion clientVersion; ClientVersion clientVersion;
@ -277,14 +294,9 @@ namespace Bloxstrap
{ {
clientVersion = await Deployment.GetInfo(); clientVersion = await Deployment.GetInfo();
} }
catch (HttpRequestException ex) catch (InvalidChannelException ex)
{ {
if (ex.StatusCode is not HttpStatusCode.Unauthorized App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}");
and not HttpStatusCode.Forbidden
and not HttpStatusCode.NotFound)
throw;
App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {Deployment.Channel} to {Deployment.DefaultChannel} because HTTP {(int)ex.StatusCode}");
Deployment.Channel = Deployment.DefaultChannel; Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo(); clientVersion = await Deployment.GetInfo();
@ -292,7 +304,7 @@ namespace Bloxstrap
if (clientVersion.IsBehindDefaultChannel) if (clientVersion.IsBehindDefaultChannel)
{ {
App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {Deployment.Channel} to {Deployment.DefaultChannel} because channel is behind production"); App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because it's behind production");
Deployment.Channel = Deployment.DefaultChannel; Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo(); clientVersion = await Deployment.GetInfo();
@ -314,20 +326,15 @@ namespace Bloxstrap
SetStatus(Strings.Bootstrapper_Status_Starting); SetStatus(Strings.Bootstrapper_Status_Starting);
if (_launchMode == LaunchMode.Player) if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage)
{
if (App.Settings.Prop.ForceRobloxLanguage)
{ {
var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
if (match.Groups.Count == 2) if (match.Groups.Count == 2)
_launchCommandLine = _launchCommandLine.Replace("robloxLocale:en_us", $"robloxLocale:{match.Groups[1].Value}", StringComparison.InvariantCultureIgnoreCase); _launchCommandLine = _launchCommandLine.Replace(
} "robloxLocale:en_us",
$"robloxLocale:{match.Groups[1].Value}",
if (!String.IsNullOrEmpty(_launchCommandLine)) StringComparison.OrdinalIgnoreCase);
_launchCommandLine += " ";
_launchCommandLine += "-isInstallerLaunch";
} }
var startInfo = new ProcessStartInfo() var startInfo = new ProcessStartInfo()
@ -337,7 +344,12 @@ namespace Bloxstrap
WorkingDirectory = AppData.Directory WorkingDirectory = AppData.Directory
}; };
if (_launchMode == LaunchMode.StudioAuth) if (_launchMode == LaunchMode.Player && ShouldRunAsAdmin())
{
startInfo.Verb = "runas";
startInfo.UseShellExecute = true;
}
else if (_launchMode == LaunchMode.StudioAuth)
{ {
Process.Start(startInfo); Process.Start(startInfo);
return; return;
@ -345,10 +357,6 @@ namespace Bloxstrap
string? logFileName = null; string? logFileName = null;
using (var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent))
{
startEvent.Reset();
string rbxLogDir = Path.Combine(Paths.LocalAppData, "Roblox\\logs"); string rbxLogDir = Path.Combine(Paths.LocalAppData, "Roblox\\logs");
if (!Directory.Exists(rbxLogDir)) if (!Directory.Exists(rbxLogDir))
@ -376,6 +384,11 @@ namespace Bloxstrap
using var process = Process.Start(startInfo)!; using var process = Process.Start(startInfo)!;
_appPid = process.Id; _appPid = process.Id;
} }
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// 1223 = ERROR_CANCELLED, gets thrown if a UAC prompt is cancelled
return;
}
catch (Exception) catch (Exception)
{ {
// attempt a reinstall on next launch // attempt a reinstall on next launch
@ -383,14 +396,9 @@ namespace Bloxstrap
throw; throw;
} }
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for start event"); App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for log file");
if (startEvent.WaitOne(TimeSpan.FromSeconds(5))) logCreatedEvent.WaitOne(TimeSpan.FromSeconds(15));
App.Logger.WriteLine(LOG_IDENT, "Start event signalled");
else
App.Logger.WriteLine(LOG_IDENT, "Start event not signalled, implying successful launch");
logCreatedEvent.WaitOne(TimeSpan.FromSeconds(5));
if (String.IsNullOrEmpty(logFileName)) if (String.IsNullOrEmpty(logFileName))
{ {
@ -404,7 +412,6 @@ namespace Bloxstrap
} }
_mutex?.ReleaseAsync(); _mutex?.ReleaseAsync();
}
if (IsStudioLaunch) if (IsStudioLaunch)
return; return;
@ -461,6 +468,27 @@ namespace Bloxstrap
if (ipl.IsAcquired) if (ipl.IsAcquired)
Process.Start(Paths.Process, args); 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() public void Cancel()
@ -636,21 +664,41 @@ namespace Bloxstrap
if (Directory.Exists(AppData.Directory)) if (Directory.Exists(AppData.Directory))
{ {
if (Directory.Exists(AppData.OldDirectory))
Directory.Delete(AppData.OldDirectory, true);
try try
{ {
// gross hack to see if roblox is still running // test to see if any files are in use
// i don't want to rely on mutexes because they can change, and will false flag for // if you have a better way to check for this, please let me know!
// running installations that are not by bloxstrap Directory.Move(AppData.Directory, AppData.OldDirectory);
File.Delete(AppData.ExecutablePath);
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine(LOG_IDENT, "Could not delete executable/folder, Roblox may still be running. Aborting update."); App.Logger.WriteLine(LOG_IDENT, "Could not clear old files, aborting update.");
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
// 0x80070020 is the HRESULT that indicates that a process is still running
// (either RobloxPlayerBeta or RobloxCrashHandler), so we'll silently ignore it
if ((uint)ex.HResult != 0x80070020)
{
// ensure no files are marked as read-only for good measure
foreach (var file in Directory.GetFiles(AppData.Directory, "*", SearchOption.AllDirectories))
Filesystem.AssertReadOnly(file);
Frontend.ShowMessageBox(
Strings.Bootstrapper_FilesInUse,
_mustUpgrade ? MessageBoxImage.Error : MessageBoxImage.Warning
);
if (_mustUpgrade)
App.Terminate(ErrorCode.ERROR_CANCELLED);
}
return; return;
} }
Directory.Delete(AppData.Directory, true); Directory.Delete(AppData.OldDirectory, true);
} }
_isInstalling = true; _isInstalling = true;
@ -1010,6 +1058,8 @@ namespace Bloxstrap
if (_cancelTokenSource.IsCancellationRequested) if (_cancelTokenSource.IsCancellationRequested)
return; return;
Directory.CreateDirectory(Paths.Downloads);
string packageUrl = Deployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); string packageUrl = Deployment.GetLocation($"/{_latestVersionGuid}-{package.Name}");
string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature); string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature);
@ -1053,9 +1103,6 @@ namespace Bloxstrap
const int maxTries = 5; const int maxTries = 5;
bool statIsRetrying = false;
bool statIsHttp = false;
App.Logger.WriteLine(LOG_IDENT, "Downloading..."); App.Logger.WriteLine(LOG_IDENT, "Downloading...");
var buffer = new byte[4096]; var buffer = new byte[4096];
@ -1108,8 +1155,6 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})"); App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})");
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
statIsRetrying = true;
if (ex.GetType() == typeof(ChecksumFailedException)) if (ex.GetType() == typeof(ChecksumFailedException))
{ {
App.SendStat("packageDownloadState", "httpFail"); App.SendStat("packageDownloadState", "httpFail");
@ -1139,20 +1184,24 @@ namespace Bloxstrap
{ {
App.Logger.WriteLine(LOG_IDENT, "Retrying download over HTTP..."); App.Logger.WriteLine(LOG_IDENT, "Retrying download over HTTP...");
packageUrl = packageUrl.Replace("https://", "http://"); packageUrl = packageUrl.Replace("https://", "http://");
statIsHttp = true;
} }
} }
} }
if (statIsRetrying)
App.SendStat("packageDownloadState", statIsHttp ? "httpSuccess" : "retrySuccess");
} }
private void ExtractPackage(Package package, List<string>? files = null) private void ExtractPackage(Package package, List<string>? files = null)
{ {
const string LOG_IDENT = "Bootstrapper::ExtractPackage"; const string LOG_IDENT = "Bootstrapper::ExtractPackage";
string packageFolder = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); 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(AppData.Directory, packageDir);
string? fileFilter = null; string? fileFilter = null;
// for sharpziplib, each file in the filter needs to be a regex // for sharpziplib, each file in the filter needs to be a regex

View File

@ -17,6 +17,8 @@
[EnumName(StaticName = "2022")] [EnumName(StaticName = "2022")]
Icon2022, Icon2022,
[EnumName(FromTranslation = "Common.Custom")] [EnumName(FromTranslation = "Common.Custom")]
IconCustom IconCustom,
[EnumName(FromTranslation = "Enums.BootstrapperStyle.ClassicFluentDialog")]
IconBloxstrapClassic
} }
} }

View File

@ -0,0 +1,10 @@
namespace Bloxstrap.Exceptions
{
public class InvalidChannelException : Exception
{
public HttpStatusCode? StatusCode;
public InvalidChannelException(HttpStatusCode? statusCode) : base()
=> StatusCode = statusCode;
}
}

View File

@ -14,6 +14,7 @@ namespace Bloxstrap.Extensions
BootstrapperIcon.IconEarly2015, BootstrapperIcon.IconEarly2015,
BootstrapperIcon.Icon2011, BootstrapperIcon.Icon2011,
BootstrapperIcon.Icon2008, BootstrapperIcon.Icon2008,
BootstrapperIcon.IconBloxstrapClassic,
BootstrapperIcon.IconCustom BootstrapperIcon.IconCustom
}; };
@ -61,6 +62,7 @@ namespace Bloxstrap.Extensions
BootstrapperIcon.Icon2017 => Properties.Resources.Icon2017, BootstrapperIcon.Icon2017 => Properties.Resources.Icon2017,
BootstrapperIcon.Icon2019 => Properties.Resources.Icon2019, BootstrapperIcon.Icon2019 => Properties.Resources.Icon2019,
BootstrapperIcon.Icon2022 => Properties.Resources.Icon2022, BootstrapperIcon.Icon2022 => Properties.Resources.Icon2022,
BootstrapperIcon.IconBloxstrapClassic => Properties.Resources.IconBloxstrapClassic,
_ => Properties.Resources.IconBloxstrap _ => Properties.Resources.IconBloxstrap
}; };
} }

View File

@ -39,17 +39,17 @@ namespace Bloxstrap
{ "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" }, { "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" },
{ "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" }, //{ "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" },
{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" }, //{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" },
{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" }, //{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" },
{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" }, //{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" },
{ "UI.Menu.Style.ReportButtonCutOff", "FFlagFixReportButtonCutOff" }, //{ "UI.Menu.Style.ReportButtonCutOff", "FFlagFixReportButtonCutOff" },
{ "UI.Menu.Style.ABTest.1", "FFlagEnableMenuControlsABTest" }, //{ "UI.Menu.Style.ABTest.1", "FFlagEnableMenuControlsABTest" },
{ "UI.Menu.Style.ABTest.2", "FFlagEnableV3MenuABTest3" }, //{ "UI.Menu.Style.ABTest.2", "FFlagEnableV3MenuABTest3" },
{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" }, //{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" },
{ "UI.Menu.Style.ABTest.4", "FFlagEnableInGameMenuChromeABTest4" } //{ "UI.Menu.Style.ABTest.4", "FFlagEnableInGameMenuChromeABTest4" }
}; };
public static IReadOnlyDictionary<RenderingMode, string> RenderingModes => new Dictionary<RenderingMode, string> public static IReadOnlyDictionary<RenderingMode, string> RenderingModes => new Dictionary<RenderingMode, string>
@ -86,68 +86,68 @@ namespace Bloxstrap
// this is one hell of a dictionary definition lmao // 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? // since these all set the same flags, wouldn't making this use bitwise operators be better?
public static IReadOnlyDictionary<InGameMenuVersion, Dictionary<string, string?>> IGMenuVersions => new Dictionary<InGameMenuVersion, Dictionary<string, string?>> //public static IReadOnlyDictionary<InGameMenuVersion, Dictionary<string, string?>> IGMenuVersions => new Dictionary<InGameMenuVersion, Dictionary<string, string?>>
{ //{
{ // {
InGameMenuVersion.Default, // InGameMenuVersion.Default,
new Dictionary<string, string?> // new Dictionary<string, string?>
{ // {
{ "V2Rollout", null }, // { "V2Rollout", null },
{ "EnableV4", null }, // { "EnableV4", null },
{ "EnableV4Chrome", null }, // { "EnableV4Chrome", null },
{ "ABTest", null }, // { "ABTest", null },
{ "ReportButtonCutOff", null } // { "ReportButtonCutOff", null }
} // }
}, // },
{ // {
InGameMenuVersion.V1, // InGameMenuVersion.V1,
new Dictionary<string, string?> // new Dictionary<string, string?>
{ // {
{ "V2Rollout", "0" }, // { "V2Rollout", "0" },
{ "EnableV4", "False" }, // { "EnableV4", "False" },
{ "EnableV4Chrome", "False" }, // { "EnableV4Chrome", "False" },
{ "ABTest", "False" }, // { "ABTest", "False" },
{ "ReportButtonCutOff", "False" } // { "ReportButtonCutOff", "False" }
} // }
}, // },
{ // {
InGameMenuVersion.V2, // InGameMenuVersion.V2,
new Dictionary<string, string?> // new Dictionary<string, string?>
{ // {
{ "V2Rollout", "100" }, // { "V2Rollout", "100" },
{ "EnableV4", "False" }, // { "EnableV4", "False" },
{ "EnableV4Chrome", "False" }, // { "EnableV4Chrome", "False" },
{ "ABTest", "False" }, // { "ABTest", "False" },
{ "ReportButtonCutOff", null } // { "ReportButtonCutOff", null }
} // }
}, // },
{ // {
InGameMenuVersion.V4, // InGameMenuVersion.V4,
new Dictionary<string, string?> // new Dictionary<string, string?>
{ // {
{ "V2Rollout", "0" }, // { "V2Rollout", "0" },
{ "EnableV4", "True" }, // { "EnableV4", "True" },
{ "EnableV4Chrome", "False" }, // { "EnableV4Chrome", "False" },
{ "ABTest", "False" }, // { "ABTest", "False" },
{ "ReportButtonCutOff", null } // { "ReportButtonCutOff", null }
} // }
}, // },
{ // {
InGameMenuVersion.V4Chrome, // InGameMenuVersion.V4Chrome,
new Dictionary<string, string?> // new Dictionary<string, string?>
{ // {
{ "V2Rollout", "0" }, // { "V2Rollout", "0" },
{ "EnableV4", "True" }, // { "EnableV4", "True" },
{ "EnableV4Chrome", "True" }, // { "EnableV4Chrome", "True" },
{ "ABTest", "False" }, // { "ABTest", "False" },
{ "ReportButtonCutOff", null } // { "ReportButtonCutOff", null }
} // }
} // }
}; //};
// all fflags are stored as strings // all fflags are stored as strings
// to delete a flag, set the value as null // to delete a flag, set the value as null
@ -243,9 +243,11 @@ namespace Bloxstrap
// clone the dictionary // clone the dictionary
OriginalProp = new(Prop); OriginalProp = new(Prop);
// TODO - remove when activity tracking has been revamped
if (GetPreset("Network.Log") != "7") if (GetPreset("Network.Log") != "7")
SetPreset("Network.Log", "7"); SetPreset("Network.Log", "7");
if (GetPreset("Rendering.ManualFullscreen") != "False")
SetPreset("Rendering.ManualFullscreen", "False");
} }
} }
} }

View File

@ -359,8 +359,6 @@ namespace Bloxstrap
|| Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp"))
|| Paths.Process.StartsWith(Paths.TempUpdates); || Paths.Process.StartsWith(Paths.TempUpdates);
isAutoUpgrade = true;
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion; var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion;
@ -592,6 +590,21 @@ namespace Bloxstrap
} }
} }
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);
}
App.Settings.Save(); App.Settings.Save();
App.FastFlags.Save(); App.FastFlags.Save();
} }

View File

@ -102,8 +102,6 @@
await Task.Delay(1000); await Task.Delay(1000);
} }
OnLogOpen?.Invoke(this, EventArgs.Empty);
LogLocation = logFileInfo.FullName; LogLocation = logFileInfo.FullName;
} }
else else
@ -111,6 +109,8 @@
logFileInfo = new FileInfo(LogLocation); logFileInfo = new FileInfo(LogLocation);
} }
OnLogOpen?.Invoke(this, EventArgs.Empty);
var logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); var logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}"); App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");

View File

@ -161,6 +161,8 @@ namespace Bloxstrap
if (process is not null) if (process is not null)
PInvoke.SetForegroundWindow((HWND)process.MainWindowHandle); PInvoke.SetForegroundWindow((HWND)process.MainWindowHandle);
App.Terminate();
} }
} }
@ -206,18 +208,18 @@ namespace Bloxstrap
// start bootstrapper and show the bootstrapper modal if we're not running silently // start bootstrapper and show the bootstrapper modal if we're not running silently
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
var bootstrapper = new Bootstrapper(launchMode); App.Bootstrapper = new Bootstrapper(launchMode);
IBootstrapperDialog? dialog = null; IBootstrapperDialog? dialog = null;
if (!App.LaunchSettings.QuietFlag.Active) if (!App.LaunchSettings.QuietFlag.Active)
{ {
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog"); App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog");
dialog = App.Settings.Prop.BootstrapperStyle.GetNew(); dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
bootstrapper.Dialog = dialog; App.Bootstrapper.Dialog = dialog;
dialog.Bootstrapper = bootstrapper; dialog.Bootstrapper = App.Bootstrapper;
} }
Task.Run(bootstrapper.Run).ContinueWith(t => Task.Run(App.Bootstrapper.Run).ContinueWith(t =>
{ {
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");

View File

@ -72,6 +72,19 @@ namespace Bloxstrap
_flagMap.Add(identifier, flag); _flagMap.Add(identifier, flag);
} }
// infer roblox launch uris
if (Args.Length >= 1)
{
string arg = Args[0];
if (arg.StartsWith("roblox:", StringComparison.OrdinalIgnoreCase)
|| arg.StartsWith("roblox-player:", StringComparison.OrdinalIgnoreCase))
{
RobloxLaunchMode = LaunchMode.Player;
RobloxLaunchArgs = arg;
}
}
// parse // parse
for (int i = 0; i < Args.Length; i++) for (int i = 0; i < Args.Length; i++)
{ {

View File

@ -12,6 +12,8 @@
public bool NoWriteMode = false; public bool NoWriteMode = false;
public string? FileLocation; public string? FileLocation;
public string AsDocument => String.Join('\n', History);
public void Initialize(bool useTempDir = false) public void Initialize(bool useTempDir = false)
{ {
const string LOG_IDENT = "Logger::Initialize"; const string LOG_IDENT = "Logger::Initialize";
@ -115,7 +117,9 @@
{ {
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
WriteLine($"[{identifier}] {ex}"); string hresult = "0x" + ex.HResult.ToString("X8");
WriteLine($"[{identifier}] ({hresult}) {ex}");
Thread.CurrentThread.CurrentUICulture = Locale.CurrentCulture; Thread.CurrentThread.CurrentUICulture = Locale.CurrentCulture;
} }

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.Models.SettingTasks
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
await using var fileStream = new FileStream(_filePath, FileMode.CreateNew); await using var fileStream = new FileStream(_filePath, FileMode.Create);
await response.Content.CopyToAsync(fileStream); await response.Content.CopyToAsync(fileStream);
OriginalState = NewState; OriginalState = NewState;

View File

@ -195,5 +195,17 @@ namespace Bloxstrap.Properties {
return ((System.Drawing.Icon)(obj)); return ((System.Drawing.Icon)(obj));
} }
} }
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon IconBloxstrapClassic
{
get
{
object obj = ResourceManager.GetObject("IconBloxstrapClassic", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
} }
} }

View File

@ -154,4 +154,7 @@
<data name="IconLate2015" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="IconLate2015" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\IconLate2015.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\IconLate2015.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="IconBloxstrapClassic" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\IconBloxstrapClassic.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root> </root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -70,7 +70,8 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!. /// Looks up a localized string similar to These are the people who&apos;ve supported Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!
///Every person here is ranked by their overall pledge..
/// </summary> /// </summary>
public static string About_Supporters_Description { public static string About_Supporters_Description {
get { get {
@ -168,6 +169,17 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Bloxstrap tried to upgrade Roblox but can&apos;t because Roblox&apos;s files are still in use.
///
///Please close any applications that may be using Roblox&apos;s files, and relaunch..
/// </summary>
public static string Bootstrapper_FilesInUse {
get {
return ResourceManager.GetString("Bootstrapper.FilesInUse", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to You must first install Bloxstrap before uninstalling.. /// Looks up a localized string similar to You must first install Bloxstrap before uninstalling..
/// </summary> /// </summary>
@ -420,6 +432,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
public static string Common_Export {
get {
return ResourceManager.GetString("Common.Export", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Help. /// Looks up a localized string similar to Help.
/// </summary> /// </summary>
@ -817,6 +838,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to A connection could not be made, which likely indicates a poor internet connection or a firewall block. If your connection is fine, please ensure that your antivirus isn&apos;t blocking Bloxstrap..
/// </summary>
public static string Dialog_Connectivity_BadConnection {
get {
return ResourceManager.GetString("Dialog.Connectivity.BadConnection", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to More information:. /// Looks up a localized string similar to More information:.
/// </summary> /// </summary>
@ -826,15 +856,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Something is likely preventing Bloxstrap from connecting to the internet..
/// </summary>
public static string Dialog_Connectivity_Preventing {
get {
return ResourceManager.GetString("Dialog.Connectivity.Preventing", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Roblox may be down right now. See {0} for more information.. /// Looks up a localized string similar to Roblox may be down right now. See {0} for more information..
/// </summary> /// </summary>
@ -862,15 +883,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to The connection timed out, which could indicate a poor internet connection or a firewall block..
/// </summary>
public static string Dialog_Connectivity_TimedOut {
get {
return ResourceManager.GetString("Dialog.Connectivity.TimedOut", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Connectivity error. /// Looks up a localized string similar to Connectivity error.
/// </summary> /// </summary>
@ -974,6 +986,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Version {0}.
/// </summary>
public static string Dialog_Exception_Version {
get {
return ResourceManager.GetString("Dialog.Exception.Version", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The chosen bootstrapper icon could not be loaded. /// Looks up a localized string similar to The chosen bootstrapper icon could not be loaded.
/// ///
@ -1022,7 +1043,9 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Please read the following help information, which will open in your web browser when you close this dialog.. /// Looks up a localized string similar to For information about why this could be happening and how this can be resolved, please read [this help article]({0}).
///
///Check if Roblox works with [the original launcher]({1}). If it doesn&apos;t, then this isn&apos;t a Bloxstrap issue. If it does, then refer to the help article..
/// </summary> /// </summary>
public static string Dialog_PlayerError_HelpInformation { public static string Dialog_PlayerError_HelpInformation {
get { get {
@ -1363,6 +1386,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Zip archive.
/// </summary>
public static string FileTypes_ZipArchive {
get {
return ResourceManager.GetString("FileTypes.ZipArchive", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Bloxstrap has been upgraded to v{0}. /// Looks up a localized string similar to Bloxstrap has been upgraded to v{0}.
/// </summary> /// </summary>
@ -2089,6 +2121,42 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Gather information that can be uploaded online to troubleshoot a problem you&apos;re having..
/// </summary>
public static string Menu_Bloxstrap_ExportData_Description {
get {
return ResourceManager.GetString("Menu.Bloxstrap.ExportData.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap configuration.
/// </summary>
public static string Menu_Bloxstrap_ExportData_ExportConfig {
get {
return ResourceManager.GetString("Menu.Bloxstrap.ExportData.ExportConfig", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All Bloxstrap logs.
/// </summary>
public static string Menu_Bloxstrap_ExportData_ExportLogs {
get {
return ResourceManager.GetString("Menu.Bloxstrap.ExportData.ExportLogs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export diagnostic data.
/// </summary>
public static string Menu_Bloxstrap_ExportData_Title {
get {
return ResourceManager.GetString("Menu.Bloxstrap.ExportData.Title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Add new. /// Looks up a localized string similar to Add new.
/// </summary> /// </summary>
@ -2745,7 +2813,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Roblox will fully close when you leave a game instead of dropping you back into the app.. /// Looks up a localized string similar to Roblox will fully close when you leave a game instead of going back to the app. [Will break some things!]({0}).
/// </summary> /// </summary>
public static string Menu_Integrations_DesktopApp_Description { public static string Menu_Integrations_DesktopApp_Description {
get { get {
@ -2898,7 +2966,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Font size can be adjusted in the Fast Flags tab.. /// Looks up a localized string similar to Font size can be adjusted in the Engine Settings tab..
/// </summary> /// </summary>
public static string Menu_Mods_Misc_CustomFont_Description { public static string Menu_Mods_Misc_CustomFont_Description {
get { get {

View File

@ -123,14 +123,11 @@
<data name="Bootstrapper.ConfirmLaunch" xml:space="preserve"> <data name="Bootstrapper.ConfirmLaunch" xml:space="preserve">
<value>Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching?</value> <value>Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching?</value>
</data> </data>
<data name="Dialog.Connectivity.Preventing" xml:space="preserve">
<value>Something is likely preventing Bloxstrap from connecting to the internet.</value>
</data>
<data name="Dialog.Connectivity.RobloxDown" xml:space="preserve"> <data name="Dialog.Connectivity.RobloxDown" xml:space="preserve">
<value>Roblox may be down right now. See {0} for more information.</value> <value>Roblox may be down right now. See {0} for more information.</value>
</data> </data>
<data name="Dialog.Connectivity.TimedOut" xml:space="preserve"> <data name="Dialog.Connectivity.BadConnection" xml:space="preserve">
<value>The connection timed out, which could indicate a poor internet connection or a firewall block.</value> <value>A connection could not be made, which likely indicates a poor internet connection or a firewall block. If your connection is fine, please ensure that your antivirus isn't blocking Bloxstrap.</value>
</data> </data>
<data name="Bootstrapper.FirstRunUninstall" xml:space="preserve"> <data name="Bootstrapper.FirstRunUninstall" xml:space="preserve">
<value>You must first install Bloxstrap before uninstalling.</value> <value>You must first install Bloxstrap before uninstalling.</value>
@ -717,7 +714,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Configure additional functionality to go alongside Roblox.</value> <value>Configure additional functionality to go alongside Roblox.</value>
</data> </data>
<data name="Menu.Integrations.DesktopApp.Description" xml:space="preserve"> <data name="Menu.Integrations.DesktopApp.Description" xml:space="preserve">
<value>Roblox will fully close when you leave a game instead of dropping you back into the app.</value> <value>Roblox will fully close when you leave a game instead of going back to the app. [Will break some things!]({0})</value>
</data> </data>
<data name="Menu.Integrations.DesktopApp.Title" xml:space="preserve"> <data name="Menu.Integrations.DesktopApp.Title" xml:space="preserve">
<value>Don't exit to desktop app</value> <value>Don't exit to desktop app</value>
@ -757,7 +754,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Choose font...</value> <value>Choose font...</value>
</data> </data>
<data name="Menu.Mods.Misc.CustomFont.Description" xml:space="preserve"> <data name="Menu.Mods.Misc.CustomFont.Description" xml:space="preserve">
<value>Font size can be adjusted in the Fast Flags tab.</value> <value>Font size can be adjusted in the Engine Settings tab.</value>
</data> </data>
<data name="Menu.Mods.Misc.CustomFont.Invalid" xml:space="preserve"> <data name="Menu.Mods.Misc.CustomFont.Invalid" xml:space="preserve">
<value>The file you have chosen does not appear to be a valid font file.</value> <value>The file you have chosen does not appear to be a valid font file.</value>
@ -906,6 +903,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
</data> </data>
<data name="FileTypes.JSONFiles" xml:space="preserve"> <data name="FileTypes.JSONFiles" xml:space="preserve">
<value>JSON files</value> <value>JSON files</value>
<comment>Shown in the open file dialog, where the file type selection dropdown is, e.g. "JSON files (*.json)"</comment>
</data> </data>
<data name="Menu.FastFlagEditor.InvalidBoolValue" xml:space="preserve"> <data name="Menu.FastFlagEditor.InvalidBoolValue" xml:space="preserve">
<value>The entry for '{0}' is not valid as the value must be a boolean (either 'True' or 'False')</value> <value>The entry for '{0}' is not valid as the value must be a boolean (either 'True' or 'False')</value>
@ -1104,7 +1102,9 @@ Are you sure you want to continue?</value>
<value>Roblox has crashed.</value> <value>Roblox has crashed.</value>
</data> </data>
<data name="Dialog.PlayerError.HelpInformation" xml:space="preserve"> <data name="Dialog.PlayerError.HelpInformation" xml:space="preserve">
<value>Please read the following help information, which will open in your web browser when you close this dialog.</value> <value>For information about why this could be happening and how this can be resolved, please read [this help article]({0}).
Check if Roblox works with [the original launcher]({1}). If it doesn't, then this isn't a Bloxstrap issue. If it does, then refer to the help article.</value>
</data> </data>
<data name="Common.NetworkError" xml:space="preserve"> <data name="Common.NetworkError" xml:space="preserve">
<value>Could not load data because of a network error.</value> <value>Could not load data because of a network error.</value>
@ -1116,7 +1116,8 @@ Are you sure you want to continue?</value>
<value>Supporters</value> <value>Supporters</value>
</data> </data>
<data name="About.Supporters.Description" xml:space="preserve"> <data name="About.Supporters.Description" xml:space="preserve">
<value>These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!</value> <value>These are the people who've supported Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!
Every person here is ranked by their overall pledge.</value>
</data> </data>
<data name="JsonManager.SettingsLoadFailed" xml:space="preserve"> <data name="JsonManager.SettingsLoadFailed" xml:space="preserve">
<value>Your Settings could not be loaded. They have been reset to the default configuration.</value> <value>Your Settings could not be loaded. They have been reset to the default configuration.</value>
@ -1239,4 +1240,35 @@ Would you like to enable test mode?</value>
<data name="LaunchMenu.LaunchRobloxStudio" xml:space="preserve"> <data name="LaunchMenu.LaunchRobloxStudio" xml:space="preserve">
<value>Launch Roblox Studio</value> <value>Launch Roblox Studio</value>
</data> </data>
<data name="Dialog.Exception.Version" xml:space="preserve">
<value>Version {0}</value>
</data>
<data name="Bootstrapper.FilesInUse" xml:space="preserve">
<value>Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use.
Please close any applications that may be using Roblox's files, and relaunch.</value>
<comment>This is *not* for when Roblox is still running when trying to upgrade. This applies to files being open (i.e. image assets)</comment>
</data>
<data name="FileTypes.ZipArchive" xml:space="preserve">
<value>Zip archive</value>
<comment>Shown in the save file dialog, where the file type selection dropdown is, e.g. "Zip archive (*.zip)"</comment>
</data>
<data name="Common.Export" xml:space="preserve">
<value>Export</value>
<comment>Currently used under the "Bloxstrap" settings tab for the button to export diagnostic data</comment>
</data>
<data name="Menu.Bloxstrap.ExportData.Title" xml:space="preserve">
<value>Export diagnostic data</value>
</data>
<data name="Menu.Bloxstrap.ExportData.Description" xml:space="preserve">
<value>Gather information that can be uploaded online to troubleshoot a problem you're having.</value>
</data>
<data name="Menu.Bloxstrap.ExportData.ExportConfig" xml:space="preserve">
<value>Bloxstrap configuration</value>
<comment>Label that appears next to a checkbox</comment>
</data>
<data name="Menu.Bloxstrap.ExportData.ExportLogs" xml:space="preserve">
<value>All Bloxstrap logs</value>
<comment>Label that appears next to a checkbox</comment>
</data>
</root> </root>

View File

@ -10,10 +10,17 @@
public static string BinaryType = "WindowsPlayer"; public static string BinaryType = "WindowsPlayer";
public static bool IsDefaultChannel => String.Compare(Channel, DefaultChannel, StringComparison.OrdinalIgnoreCase) == 0; public static bool IsDefaultChannel => Channel.Equals(DefaultChannel, StringComparison.OrdinalIgnoreCase);
public static string BaseUrl { get; private set; } = null!; public static string BaseUrl { get; private set; } = null!;
public static readonly List<HttpStatusCode?> BadChannelCodes = new()
{
HttpStatusCode.Unauthorized,
HttpStatusCode.Forbidden,
HttpStatusCode.NotFound
};
private static readonly Dictionary<string, ClientVersion> ClientVersionCache = new(); private static readonly Dictionary<string, ClientVersion> ClientVersionCache = new();
// a list of roblox deployment locations that we check for, in case one of them don't work // a list of roblox deployment locations that we check for, in case one of them don't work
@ -86,7 +93,7 @@
if (finishedTask.IsFaulted) if (finishedTask.IsFaulted)
exceptions.Add(finishedTask.Exception!.InnerException!); exceptions.Add(finishedTask.Exception!.InnerException!);
else else if (!finishedTask.IsCanceled)
BaseUrl = finishedTask.Result; BaseUrl = finishedTask.Result;
} }
@ -94,8 +101,14 @@
tokenSource.Cancel(); tokenSource.Cancel();
if (string.IsNullOrEmpty(BaseUrl)) if (string.IsNullOrEmpty(BaseUrl))
{
if (exceptions.Any())
return exceptions[0]; return exceptions[0];
// task cancellation exceptions don't get added to the list
return new TaskCanceledException("All connection attempts timed out.");
}
App.Logger.WriteLine(LOG_IDENT, $"Got {BaseUrl} as the optimal base URL"); App.Logger.WriteLine(LOG_IDENT, $"Got {BaseUrl} as the optimal base URL");
return null; return null;
@ -153,6 +166,11 @@
{ {
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettingscdn.roblox.com" + path); clientVersion = await Http.GetJson<ClientVersion>("https://clientsettingscdn.roblox.com" + path);
} }
catch (HttpRequestException httpEx)
when (!isDefaultChannel && BadChannelCodes.Contains(httpEx.StatusCode))
{
throw new InvalidChannelException(httpEx.StatusCode);
}
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings..."); App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");

View File

@ -10,6 +10,7 @@
xmlns:resources="clr-namespace:Bloxstrap.Resources" xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800" d:DesignHeight="1500" d:DesignWidth="800"
SizeChanged="UiPage_SizeChanged"
Title="AboutPage" Title="AboutPage"
Scrollable="True"> Scrollable="True">
<StackPanel Margin="0,0,14,14"> <StackPanel Margin="0,0,14,14">
@ -93,7 +94,7 @@
</ListView.ItemTemplate> </ListView.ItemTemplate>
<ListView.ItemsPanel> <ListView.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<UniformGrid Columns="{Binding SupporterData.Monthly.Columns}" Margin="-4" /> <UniformGrid Columns="{Binding Columns}" Margin="-4" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ListView.ItemsPanel> </ListView.ItemsPanel>
</ListView> </ListView>
@ -122,7 +123,7 @@
</ListView.ItemTemplate> </ListView.ItemTemplate>
<ListView.ItemsPanel> <ListView.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<UniformGrid Columns="{Binding SupporterData.OneOff.Columns}" Margin="-4" /> <UniformGrid Columns="{Binding Columns}" Margin="-4" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ListView.ItemsPanel> </ListView.ItemsPanel>
</ListView> </ListView>

View File

@ -1,4 +1,6 @@
using Bloxstrap.UI.ViewModels.About; using System.Windows;
using Bloxstrap.UI.ViewModels.About;
namespace Bloxstrap.UI.Elements.About.Pages namespace Bloxstrap.UI.Elements.About.Pages
{ {
@ -7,10 +9,15 @@ namespace Bloxstrap.UI.Elements.About.Pages
/// </summary> /// </summary>
public partial class SupportersPage public partial class SupportersPage
{ {
private readonly SupportersViewModel _viewModel = new();
public SupportersPage() public SupportersPage()
{ {
DataContext = new SupportersViewModel(); DataContext = _viewModel;
InitializeComponent(); InitializeComponent();
} }
private void UiPage_SizeChanged(object sender, SizeChangedEventArgs e)
=> _viewModel.WindowResizeEvent?.Invoke(sender, e);
} }
} }

View File

@ -40,11 +40,15 @@
</Grid> </Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}"> <Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<Grid>
<TextBlock x:Name="VersionText" VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="Version 2.8.0" />
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button x:Name="ReportExceptionButton" Content="{x:Static resources:Strings.Dialog_Exception_Report}" /> <Button x:Name="ReportExceptionButton" Content="{x:Static resources:Strings.Dialog_Exception_Report}" />
<Button x:Name="LocateLogFileButton" Content="{x:Static resources:Strings.Common_OpenLogFile}" Margin="12,0,0,0" /> <Button x:Name="LocateLogFileButton" Content="{x:Static resources:Strings.Common_OpenLogFile}" Margin="12,0,0,0" />
<Button x:Name="CloseButton" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Margin="12,0,0,0" /> <Button x:Name="CloseButton" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Margin="12,0,0,0" />
</StackPanel> </StackPanel>
</Grid>
</Border> </Border>
</Grid> </Grid>
</base:WpfUiWindow> </base:WpfUiWindow>

View File

@ -16,6 +16,8 @@ namespace Bloxstrap.UI.Elements.Dialogs
/// </summary> /// </summary>
public partial class ExceptionDialog public partial class ExceptionDialog
{ {
const int MAX_GITHUB_URL_LENGTH = 8192;
public ExceptionDialog(Exception exception) public ExceptionDialog(Exception exception)
{ {
InitializeComponent(); InitializeComponent();
@ -27,12 +29,19 @@ namespace Bloxstrap.UI.Elements.Dialogs
string repoUrl = $"https://github.com/{App.ProjectRepository}"; string repoUrl = $"https://github.com/{App.ProjectRepository}";
string wikiUrl = $"{repoUrl}/wiki"; string wikiUrl = $"{repoUrl}/wiki";
string issueUrl = String.Format( string title = HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}");
"{0}/issues/new?template=bug_report.yaml&title={1}&log={2}", string log = HttpUtility.UrlEncode(App.Logger.AsDocument);
repoUrl,
HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}"), string issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml&title={title}&log={log}";
HttpUtility.UrlEncode(String.Join('\n', App.Logger.History))
); if (issueUrl.Length > MAX_GITHUB_URL_LENGTH)
{
// url is way too long for github. remove the log parameter.
issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml&title={title}";
if (issueUrl.Length > MAX_GITHUB_URL_LENGTH)
issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml"; // bruh
}
string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, wikiUrl, issueUrl); string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, wikiUrl, issueUrl);
@ -40,6 +49,8 @@ namespace Bloxstrap.UI.Elements.Dialogs
helpMessage = String.Format(Strings.Dialog_Exception_Info_2_Alt, wikiUrl); helpMessage = String.Format(Strings.Dialog_Exception_Info_2_Alt, wikiUrl);
HelpMessageMDTextBlock.MarkdownText = helpMessage; HelpMessageMDTextBlock.MarkdownText = helpMessage;
VersionText.Text = String.Format(Strings.Dialog_Exception_Version, App.Version);
ReportExceptionButton.Click += (_, _) => Utilities.ShellExecute(issueUrl); ReportExceptionButton.Click += (_, _) => Utilities.ShellExecute(issueUrl);
LocateLogFileButton.Click += delegate LocateLogFileButton.Click += delegate
@ -47,7 +58,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
if (App.Logger.Initialized && !String.IsNullOrEmpty(App.Logger.FileLocation)) if (App.Logger.Initialized && !String.IsNullOrEmpty(App.Logger.FileLocation))
Utilities.ShellExecute(App.Logger.FileLocation); Utilities.ShellExecute(App.Logger.FileLocation);
else else
Clipboard.SetDataObject(String.Join("\r\n", App.Logger.History)); Clipboard.SetDataObject(App.Logger.AsDocument);
}; };
CloseButton.Click += delegate CloseButton.Click += delegate

View File

@ -6,6 +6,7 @@
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs" xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base" xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
mc:Ignorable="d" mc:Ignorable="d"
Title="Bloxstrap" Title="Bloxstrap"
d:DesignWidth="480" d:DesignWidth="480"
@ -33,7 +34,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Image x:Name="IconImage" Grid.Column="0" RenderOptions.BitmapScalingMode="HighQuality" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Top" /> <Image x:Name="IconImage" Grid.Column="0" RenderOptions.BitmapScalingMode="HighQuality" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Top" />
<TextBlock x:Name="MessageTextBlock" Grid.Column="1" VerticalAlignment="Center" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" /> <controls:MarkdownTextBlock x:Name="MessageTextBlock" Grid.Column="1" VerticalAlignment="Center" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</Grid> </Grid>
<Border Grid.Row="2" Margin="0,10,0,0" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}"> <Border Grid.Row="2" Margin="0,10,0,0" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">

View File

@ -60,6 +60,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
Title = App.ProjectName; Title = App.ProjectName;
MessageTextBlock.Text = message; MessageTextBlock.Text = message;
MessageTextBlock.MarkdownText = message;
ButtonOne.Visibility = Visibility.Collapsed; ButtonOne.Visibility = Visibility.Collapsed;
ButtonTwo.Visibility = Visibility.Collapsed; ButtonTwo.Visibility = Visibility.Collapsed;
ButtonThree.Visibility = Visibility.Collapsed; ButtonThree.Visibility = Visibility.Collapsed;

View File

@ -58,7 +58,7 @@
</Grid> </Grid>
<StackPanel Grid.Column="1" Margin="16"> <StackPanel Grid.Column="1" Margin="16">
<ui:CardAction Icon="ArrowRight12" Command="{Binding LaunchRobloxCommand, Mode=OneTime}"> <ui:CardAction Icon="ArrowRight12" TabIndex="0" Command="{Binding LaunchRobloxCommand, Mode=OneTime}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}" />
</StackPanel> </StackPanel>
@ -70,7 +70,7 @@
</StackPanel> </StackPanel>
</ui:CardAction> </ui:CardAction>
<ui:CardAction Margin="0,8,0,0" Icon="Settings28" Command="{Binding LaunchSettingsCommand, Mode=OneTime}"> <ui:CardAction Margin="0,8,0,0" TabIndex="1" Icon="Settings28" Command="{Binding LaunchSettingsCommand, Mode=OneTime}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_ConfigureSettings}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_ConfigureSettings}" />
</StackPanel> </StackPanel>
@ -78,7 +78,7 @@
<Border Margin="16" /> <Border Margin="16" />
<ui:CardAction Margin="0,8,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/bloxstraplabs/bloxstrap/wiki/"> <ui:CardAction Margin="0,8,0,0" TabIndex="2" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/bloxstraplabs/bloxstrap/wiki/">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Title}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" /> <TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />

View File

@ -31,9 +31,7 @@ namespace Bloxstrap.UI.Elements.Installer
/// - MainWindow has a single-set Func<bool> property named NextPageCallback which is reset on every page load /// - MainWindow has a single-set Func<bool> property named NextPageCallback which is reset on every page load
/// - This callback is called when the next page button is pressed /// - This callback is called when the next page button is pressed
/// - Page CodeBehind gets MainWindow and sets the callback to its own local function on page load /// - Page CodeBehind gets MainWindow and sets the callback to its own local function on page load
/// - CodeBehind's local function then directly calls the ViewModel to do whatever it needs to do /// - CodeBehind's local function then directly calls its ViewModel to do whatever it needs to do
///
/// TODO: theme selection
public partial class MainWindow : WpfUiWindow, INavigationWindow public partial class MainWindow : WpfUiWindow, INavigationWindow
{ {
@ -43,6 +41,8 @@ namespace Bloxstrap.UI.Elements.Installer
private List<Type> _pages = new() { typeof(WelcomePage), typeof(InstallPage), typeof(CompletionPage) }; private List<Type> _pages = new() { typeof(WelcomePage), typeof(InstallPage), typeof(CompletionPage) };
private DateTimeOffset _lastNavigation = DateTimeOffset.Now;
public Func<bool>? NextPageCallback; public Func<bool>? NextPageCallback;
public NextAction CloseAction = NextAction.Terminate; public NextAction CloseAction = NextAction.Terminate;
@ -55,10 +55,16 @@ namespace Bloxstrap.UI.Elements.Installer
_viewModel.PageRequest += (_, type) => _viewModel.PageRequest += (_, type) =>
{ {
// debounce
if (DateTimeOffset.Now.Subtract(_lastNavigation).TotalMilliseconds < 500)
return;
if (type == "next") if (type == "next")
NextPage(); NextPage();
else if (type == "back") else if (type == "back")
BackPage(); BackPage();
_lastNavigation = DateTimeOffset.Now;
}; };
DataContext = _viewModel; DataContext = _viewModel;

View File

@ -11,7 +11,7 @@
mc:Ignorable="d" mc:Ignorable="d"
d:DataContext="{d:DesignInstance dmodels:MainWindowViewModel, IsDesignTimeCreatable=True}" d:DataContext="{d:DesignInstance dmodels:MainWindowViewModel, IsDesignTimeCreatable=True}"
Title="{x:Static resources:Strings.Menu_Title}" Title="{x:Static resources:Strings.Menu_Title}"
MinWidth="960" MinWidth="1000"
Width="1000" Width="1000"
Height="580" Height="580"
Background="{ui:ThemeResource ApplicationBackgroundBrush}" Background="{ui:ThemeResource ApplicationBackgroundBrush}"

View File

@ -8,7 +8,7 @@
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls" xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Settings" xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Settings"
xmlns:resources="clr-namespace:Bloxstrap.Resources" xmlns:resources="clr-namespace:Bloxstrap.Resources"
d:DataContext="{d:DesignInstance Type=models:BehaviourViewModel}" d:DataContext="{d:DesignInstance Type=models:BloxstrapViewModel}"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800" d:DesignHeight="600" d:DesignWidth="800"
Title="BehaviourPage" Title="BehaviourPage"
@ -28,5 +28,44 @@
Description="{Binding Source={x:Static resources:Strings.Menu_Bloxstrap_Analytics_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://github.com/bloxstraplabs/bloxstrap/wiki/Privacy-Policy#analytical-functionality'}"> Description="{Binding Source={x:Static resources:Strings.Menu_Bloxstrap_Analytics_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://github.com/bloxstraplabs/bloxstrap/wiki/Privacy-Policy#analytical-functionality'}">
<ui:ToggleSwitch IsChecked="{Binding AnalyticsEnabled, Mode=TwoWay}" /> <ui:ToggleSwitch IsChecked="{Binding AnalyticsEnabled, Mode=TwoWay}" />
</controls:OptionControl> </controls:OptionControl>
<ui:CardExpander Margin="0,8,0,0" IsExpanded="True">
<ui:CardExpander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Bloxstrap_ExportData_Title}" />
<TextBlock FontSize="12" Text="{x:Static resources:Strings.Menu_Bloxstrap_ExportData_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel>
<ui:Button Grid.Column="1" MinWidth="100" Margin="0,0,16,0" Icon="CopySelect20" Content="{x:Static resources:Strings.Common_Export}" Command="{Binding ExportDataCommand}">
<ui:Button.Style>
<Style TargetType="ui:Button" BasedOn="{StaticResource {x:Type ui:Button}}">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=ShouldExportConfig, Path=IsChecked}" Value="False" />
<Condition Binding="{Binding ElementName=ShouldExportLogs, Path=IsChecked}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="False" />
</MultiDataTrigger>
</Style.Triggers>
<Setter Property="IsEnabled" Value="True" />
</Style>
</ui:Button.Style>
</ui:Button>
</Grid>
</ui:CardExpander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" Name="ShouldExportConfig" Content="{x:Static resources:Strings.Menu_Bloxstrap_ExportData_ExportConfig}" IsChecked="{Binding ShouldExportConfig}" />
<CheckBox Grid.Column="1" Name="ShouldExportLogs" Content="{x:Static resources:Strings.Menu_Bloxstrap_ExportData_ExportLogs}" IsChecked="{Binding ShouldExportLogs}" />
</Grid>
</ui:CardExpander>
</StackPanel> </StackPanel>
</ui:UiPage> </ui:UiPage>

View File

@ -104,7 +104,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
return; return;
if (dialog.Tabs.SelectedIndex == 0) if (dialog.Tabs.SelectedIndex == 0)
AddSingle(dialog.FlagNameTextBox.Text, dialog.FlagValueTextBox.Text); AddSingle(dialog.FlagNameTextBox.Text.Trim(), dialog.FlagValueTextBox.Text);
else if (dialog.Tabs.SelectedIndex == 1) else if (dialog.Tabs.SelectedIndex == 1)
ImportJSON(dialog.JsonTextBox.Text); ImportJSON(dialog.JsonTextBox.Text);
} }

View File

@ -10,6 +10,7 @@
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
Scrollable="True" Scrollable="True"
Loaded="Page_Loaded" Loaded="Page_Loaded"
Unloaded="Page_Unloaded"
Title="FastFlagEditorWarningPage"> Title="FastFlagEditorWarningPage">
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">

View File

@ -8,25 +8,24 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
/// </summary> /// </summary>
public partial class FastFlagEditorWarningPage public partial class FastFlagEditorWarningPage
{ {
private bool _initialLoad = false; private FastFlagEditorWarningViewModel _viewModel;
public FastFlagEditorWarningPage() public FastFlagEditorWarningPage()
{ {
DataContext = new FastFlagEditorWarningViewModel(this); _viewModel = new FastFlagEditorWarningViewModel(this);
DataContext = _viewModel;
InitializeComponent(); InitializeComponent();
} }
private void Page_Loaded(object sender, RoutedEventArgs e) private void Page_Loaded(object sender, RoutedEventArgs e)
{ {
// refresh datacontext on page load to reset timer _viewModel.StartCountdown();
}
if (!_initialLoad) private void Page_Unloaded(object sender, RoutedEventArgs e)
{ {
_initialLoad = true; _viewModel.StopCountdown();
return;
}
DataContext = new FastFlagEditorWarningViewModel(this);
} }
} }
} }

View File

@ -22,6 +22,12 @@
</StackPanel> </StackPanel>
</ui:CardAction> </ui:CardAction>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_FastFlags_ManagerEnabled_Title}"
Description="{x:Static resources:Strings.Menu_FastFlags_ManagerEnabled_Description}">
<ui:ToggleSwitch IsChecked="{Binding UseFastFlagManager, Mode=TwoWay}" />
</controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Common_Presets}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" /> <TextBlock Text="{x:Static resources:Strings.Common_Presets}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_FastFlags_Presets_Categories_Rendering}" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" /> <TextBlock Text="{x:Static resources:Strings.Menu_FastFlags_Presets_Categories_Rendering}" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
@ -125,7 +131,7 @@
<ui:TextBox Width="200" Padding="10,5,10,5" Text="{Binding FontSize, Mode=TwoWay}" PreviewTextInput="ValidateInt32" /> <ui:TextBox Width="200" Padding="10,5,10,5" Text="{Binding FontSize, Mode=TwoWay}" PreviewTextInput="ValidateInt32" />
</controls:OptionControl> </controls:OptionControl>
<controls:OptionControl <!--<controls:OptionControl
Header="{x:Static resources:Strings.Menu_FastFlags_Presets_EscapeMenuVersion_Title}" Header="{x:Static resources:Strings.Menu_FastFlags_Presets_EscapeMenuVersion_Title}"
HelpLink="https://github.com/bloxstraplabs/bloxstrap/wiki/A-guide-to-FastFlags#escape-menu-version"> HelpLink="https://github.com/bloxstraplabs/bloxstrap/wiki/A-guide-to-FastFlags#escape-menu-version">
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding IGMenuVersions.Keys, Mode=OneTime}" Text="{Binding SelectedIGMenuVersion, Mode=TwoWay}"> <ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding IGMenuVersions.Keys, Mode=OneTime}" Text="{Binding SelectedIGMenuVersion, Mode=TwoWay}">
@ -135,7 +141,7 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
</controls:OptionControl> </controls:OptionControl>-->
<controls:OptionControl <controls:OptionControl
Margin="0,24,0,0" Margin="0,24,0,0"

View File

@ -35,7 +35,8 @@
<controls:OptionControl <controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_DesktopApp_Title}" Header="{x:Static resources:Strings.Menu_Integrations_DesktopApp_Title}"
Description="{x:Static resources:Strings.Menu_Integrations_DesktopApp_Description}" Description="{Binding Source={x:Static resources:Strings.Menu_Integrations_DesktopApp_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://github.com/bloxstraplabs/bloxstrap/wiki/What-is-activity-tracking%3F#dont-exit-to-desktop-app'}"
HelpLink="https://github.com/bloxstraplabs/bloxstrap/wiki/What-is-activity-tracking%3F#dont-exit-to-desktop-app"
IsEnabled="{Binding InnerContent.IsChecked, ElementName=ActivityTrackingOption, Mode=OneWay}"> IsEnabled="{Binding InnerContent.IsChecked, ElementName=ActivityTrackingOption, Mode=OneWay}">
<ui:ToggleSwitch IsChecked="{Binding DisableAppPatchEnabled, Mode=TwoWay}" /> <ui:ToggleSwitch IsChecked="{Binding DisableAppPatchEnabled, Mode=TwoWay}" />
</controls:OptionControl> </controls:OptionControl>

View File

@ -27,9 +27,13 @@ namespace Bloxstrap.UI
if (crash) if (crash)
topLine = Strings.Dialog_PlayerError_Crash; topLine = Strings.Dialog_PlayerError_Crash;
ShowMessageBox($"{topLine}\n\n{Strings.Dialog_PlayerError_HelpInformation}", MessageBoxImage.Error); string info = String.Format(
Strings.Dialog_PlayerError_HelpInformation,
$"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch",
$"https://github.com/{App.ProjectRepository}/wiki/Switching-between-Roblox-and-Bloxstrap"
);
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch"); ShowMessageBox($"{topLine}\n\n{info}", MessageBoxImage.Error);
} }
public static void ShowExceptionDialog(Exception exception) public static void ShowExceptionDialog(Exception exception)

View File

@ -1,4 +1,6 @@
namespace Bloxstrap.UI.ViewModels.About using System.Windows;
namespace Bloxstrap.UI.ViewModels.About
{ {
public class SupportersViewModel : NotifyPropertyChangedViewModel public class SupportersViewModel : NotifyPropertyChangedViewModel
{ {
@ -8,12 +10,32 @@
public string LoadError { get; set; } = ""; public string LoadError { get; set; } = "";
public int Columns { get; set; } = 3;
public SizeChangedEventHandler? WindowResizeEvent;
public SupportersViewModel() public SupportersViewModel()
{ {
WindowResizeEvent += OnWindowResize;
// this will cause momentary freezes only when ran under the debugger // this will cause momentary freezes only when ran under the debugger
LoadSupporterData(); LoadSupporterData();
} }
private void OnWindowResize(object sender, SizeChangedEventArgs e)
{
if (!e.WidthChanged)
return;
int newCols = (int)Math.Floor(e.NewSize.Width / 200);
if (Columns == newCols)
return;
Columns = newCols;
OnPropertyChanged(nameof(Columns));
}
public async void LoadSupporterData() public async void LoadSupporterData()
{ {
const string LOG_IDENT = "AboutViewModel::LoadSupporterData"; const string LOG_IDENT = "AboutViewModel::LoadSupporterData";

View File

@ -1,4 +1,9 @@
namespace Bloxstrap.UI.ViewModels.Settings using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Win32;
namespace Bloxstrap.UI.ViewModels.Settings
{ {
public class BloxstrapViewModel : NotifyPropertyChangedViewModel public class BloxstrapViewModel : NotifyPropertyChangedViewModel
{ {
@ -13,5 +18,73 @@
get => App.Settings.Prop.EnableAnalytics; get => App.Settings.Prop.EnableAnalytics;
set => App.Settings.Prop.EnableAnalytics = value; set => App.Settings.Prop.EnableAnalytics = value;
} }
public bool ShouldExportConfig { get; set; } = true;
public bool ShouldExportLogs { get; set; } = true;
public ICommand ExportDataCommand => new RelayCommand(ExportData);
private void ExportData()
{
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
var dialog = new SaveFileDialog
{
FileName = $"Bloxstrap-export-{timestamp}.zip",
Filter = $"{Strings.FileTypes_ZipArchive}|*.zip"
};
if (dialog.ShowDialog() != true)
return;
using var memStream = new MemoryStream();
using var zipStream = new ZipOutputStream(memStream);
if (ShouldExportConfig)
{
var files = new List<string>()
{
App.Settings.FileLocation,
App.State.FileLocation,
App.FastFlags.FileLocation
};
AddFilesToZipStream(zipStream, files, "Config/");
}
if (ShouldExportLogs && Directory.Exists(Paths.Logs))
{
var files = Directory.GetFiles(Paths.Logs)
.Where(x => !x.Equals(App.Logger.FileLocation, StringComparison.OrdinalIgnoreCase));
AddFilesToZipStream(zipStream, files, "Logs/");
}
zipStream.CloseEntry();
memStream.Position = 0;
using var outputStream = File.OpenWrite(dialog.FileName);
memStream.CopyTo(outputStream);
Process.Start("explorer.exe", $"/select,\"{dialog.FileName}\"");
}
private void AddFilesToZipStream(ZipOutputStream zipStream, IEnumerable<string> files, string directory)
{
foreach (string file in files)
{
if (!File.Exists(file))
continue;
var entry = new ZipEntry(directory + Path.GetFileName(file));
entry.DateTime = DateTime.Now;
zipStream.PutNextEntry(entry);
using var fileStream = File.OpenRead(file);
fileStream.CopyTo(zipStream);
}
}
} }
} }

View File

@ -13,6 +13,8 @@ namespace Bloxstrap.UI.ViewModels.Settings
{ {
private Page _page; private Page _page;
private CancellationTokenSource? _cancellationTokenSource;
public string ContinueButtonText { get; set; } = ""; public string ContinueButtonText { get; set; } = "";
public bool CanContinue { get; set; } = false; public bool CanContinue { get; set; } = false;
@ -24,17 +26,40 @@ namespace Bloxstrap.UI.ViewModels.Settings
public FastFlagEditorWarningViewModel(Page page) public FastFlagEditorWarningViewModel(Page page)
{ {
_page = page; _page = page;
DoCountdown();
} }
private async void DoCountdown() public void StopCountdown()
{ {
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = null;
}
public void StartCountdown()
{
StopCountdown();
_cancellationTokenSource = new CancellationTokenSource();
DoCountdown(_cancellationTokenSource.Token);
}
private async void DoCountdown(CancellationToken token)
{
CanContinue = false;
OnPropertyChanged(nameof(CanContinue));
for (int i = 10; i > 0; i--) for (int i = 10; i > 0; i--)
{ {
ContinueButtonText = $"({i}) {Strings.Menu_FastFlagEditor_Warning_Continue}"; ContinueButtonText = $"({i}) {Strings.Menu_FastFlagEditor_Warning_Continue}";
OnPropertyChanged(nameof(ContinueButtonText)); OnPropertyChanged(nameof(ContinueButtonText));
await Task.Delay(1000); try
{
await Task.Delay(1000, token);
}
catch (TaskCanceledException)
{
return;
}
} }
ContinueButtonText = Strings.Menu_FastFlagEditor_Warning_Continue; ContinueButtonText = Strings.Menu_FastFlagEditor_Warning_Continue;
@ -42,9 +67,6 @@ namespace Bloxstrap.UI.ViewModels.Settings
CanContinue = true; CanContinue = true;
OnPropertyChanged(nameof(CanContinue)); OnPropertyChanged(nameof(CanContinue));
App.State.Prop.ShowFFlagEditorWarning = false;
App.State.Save();
} }
private void Continue() private void Continue()
@ -52,6 +74,9 @@ namespace Bloxstrap.UI.ViewModels.Settings
if (!CanContinue) if (!CanContinue)
return; return;
App.State.Prop.ShowFFlagEditorWarning = false;
App.State.Save(); // should we be force saving here?
if (Window.GetWindow(_page) is INavigationWindow window) if (Window.GetWindow(_page) is INavigationWindow window)
window.Navigate(typeof(FastFlagEditorPage)); window.Navigate(typeof(FastFlagEditorPage));
} }

View File

@ -1,5 +1,4 @@
using System.Windows; using System.Windows.Input;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@ -53,39 +52,39 @@ namespace Bloxstrap.UI.ViewModels.Settings
set => App.FastFlags.SetPreset("Rendering.DisableScaling", value ? "True" : null); set => App.FastFlags.SetPreset("Rendering.DisableScaling", value ? "True" : null);
} }
public IReadOnlyDictionary<InGameMenuVersion, Dictionary<string, string?>> IGMenuVersions => FastFlagManager.IGMenuVersions; //public IReadOnlyDictionary<InGameMenuVersion, Dictionary<string, string?>> IGMenuVersions => FastFlagManager.IGMenuVersions;
public InGameMenuVersion SelectedIGMenuVersion //public InGameMenuVersion SelectedIGMenuVersion
{ //{
get // get
{ // {
// yeah this kinda sucks // // yeah this kinda sucks
foreach (var version in IGMenuVersions) // foreach (var version in IGMenuVersions)
{ // {
bool flagsMatch = true; // bool flagsMatch = true;
foreach (var flag in version.Value) // foreach (var flag in version.Value)
{ // {
foreach (var presetFlag in FastFlagManager.PresetFlags.Where(x => x.Key.StartsWith($"UI.Menu.Style.{flag.Key}"))) // foreach (var presetFlag in FastFlagManager.PresetFlags.Where(x => x.Key.StartsWith($"UI.Menu.Style.{flag.Key}")))
{ // {
if (App.FastFlags.GetValue(presetFlag.Value) != flag.Value) // if (App.FastFlags.GetValue(presetFlag.Value) != flag.Value)
flagsMatch = false; // flagsMatch = false;
} // }
} // }
if (flagsMatch) // if (flagsMatch)
return version.Key; // return version.Key;
} // }
return IGMenuVersions.First().Key; // return IGMenuVersions.First().Key;
} // }
set // set
{ // {
foreach (var flag in IGMenuVersions[value]) // foreach (var flag in IGMenuVersions[value])
App.FastFlags.SetPreset($"UI.Menu.Style.{flag.Key}", flag.Value); // App.FastFlags.SetPreset($"UI.Menu.Style.{flag.Key}", flag.Value);
} // }
} //}
public IReadOnlyDictionary<LightingMode, string> LightingModes => FastFlagManager.LightingModes; public IReadOnlyDictionary<LightingMode, string> LightingModes => FastFlagManager.LightingModes;

View File

@ -41,12 +41,26 @@ namespace Bloxstrap
/// 1: version1 &gt; version2 /// 1: version1 &gt; version2
/// </returns> /// </returns>
public static VersionComparison CompareVersions(string versionStr1, string versionStr2) public static VersionComparison CompareVersions(string versionStr1, string versionStr2)
{
try
{ {
var version1 = new Version(versionStr1.Replace("v", "")); var version1 = new Version(versionStr1.Replace("v", ""));
var version2 = new Version(versionStr2.Replace("v", "")); var version2 = new Version(versionStr2.Replace("v", ""));
return (VersionComparison)version1.CompareTo(version2); return (VersionComparison)version1.CompareTo(version2);
} }
catch (Exception)
{
// temporary diagnostic log for the issue described here:
// https://github.com/bloxstraplabs/bloxstrap/issues/3193
// the problem is that this happens only on upgrade, so my only hope of catching this is bug reports following the next release
App.Logger.WriteLine("Utilities::CompareVersions", "An exception occurred when comparing versions");
App.Logger.WriteLine("Utilities::CompareVersions", $"versionStr1={versionStr1} versionStr2={versionStr2}");
throw;
}
}
public static string GetRobloxVersion(bool studio) public static string GetRobloxVersion(bool studio)
{ {

View File

@ -6,6 +6,8 @@ namespace Bloxstrap.Utility
{ {
private const string RobloxPlaceKey = "Roblox.Place"; private const string RobloxPlaceKey = "Roblox.Place";
public static readonly List<RegistryKey> Roots = new() { Registry.CurrentUser, Registry.LocalMachine };
public static void RegisterProtocol(string key, string name, string handler, string handlerParam = "%1") public static void RegisterProtocol(string key, string name, string handler, string handlerParam = "%1")
{ {
string handlerArgs = $"\"{handler}\" {handlerParam}"; string handlerArgs = $"\"{handler}\" {handlerParam}";

2
wpfui

@ -1 +1 @@
Subproject commit c4c58c589970a66b27a9de41ab1b6b6539918b52 Subproject commit 9080158ba8d496501146d1167aae910898eff9af