Draft: bootstrapper refactoring

Roblox now installs to /Roblox/Player/ instead of /Versions/<version guid>

This is a checkpoint commit. No mod manager, no error checking, no fullscreen optimizations configuration. Only installing and launching Roblox.

THIS WORKED FIRST TRY BY THE WAY
This commit is contained in:
pizzaboxer 2024-09-05 22:00:07 +01:00
parent 15dc2dfbfe
commit 3eeebc7a8b
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
18 changed files with 376 additions and 513 deletions

View File

@ -39,8 +39,17 @@ namespace Bloxstrap.AppData
{ "extracontent-places.zip", @"ExtraContent\places\" }, { "extracontent-places.zip", @"ExtraContent\places\" },
}; };
public virtual string FinalDirectory { get; } = null!;
public string StagingDirectory => $"{FinalDirectory}.staging";
public virtual string ExecutableName { get; } = null!;
public string ExecutablePath => Path.Combine(FinalDirectory, ExecutableName);
public virtual IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } public virtual IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
public CommonAppData() public CommonAppData()
{ {
if (PackageDirectoryMap is null) if (PackageDirectoryMap is null)

View File

@ -1,10 +1,4 @@
using System; namespace Bloxstrap.AppData
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.AppData
{ {
internal interface IAppData internal interface IAppData
{ {
@ -18,6 +12,14 @@ namespace Bloxstrap.AppData
string StartEvent { get; } string StartEvent { get; }
string FinalDirectory { get; }
string StagingDirectory { get; }
string ExecutablePath { get; }
AppState State { get; }
IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
} }
} }

View File

@ -8,15 +8,19 @@ namespace Bloxstrap.AppData
{ {
public class RobloxPlayerData : CommonAppData, IAppData public class RobloxPlayerData : CommonAppData, IAppData
{ {
public string ProductName { get; } = "Roblox"; public string ProductName => "Roblox";
public string BinaryType { get; } = "WindowsPlayer"; public string BinaryType => "WindowsPlayer";
public string RegistryName { get; } = "RobloxPlayer"; public string RegistryName => "RobloxPlayer";
public string ExecutableName { get; } = "RobloxPlayerBeta.exe"; public override string ExecutableName => "RobloxPlayerBeta.exe";
public string StartEvent { get; } = "www.roblox.com/robloxStartedEvent"; public string StartEvent => "www.roblox.com/robloxStartedEvent";
public override string FinalDirectory => Path.Combine(Paths.Roblox, "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

@ -1,22 +1,20 @@
using System; namespace Bloxstrap.AppData
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.AppData
{ {
public class RobloxStudioData : CommonAppData, IAppData public class RobloxStudioData : CommonAppData, IAppData
{ {
public string ProductName { get; } = "Roblox Studio"; public string ProductName => "Roblox Studio";
public string BinaryType { get; } = "WindowsStudio64"; public string BinaryType => "WindowsStudio64";
public string RegistryName { get; } = "RobloxStudio"; public string RegistryName => "RobloxStudio";
public string ExecutableName { get; } = "RobloxStudioBeta.exe"; public override string ExecutableName => "RobloxStudioBeta.exe";
public string StartEvent { get; } = "www.roblox.com/robloxStudioStartedEvent"; public string StartEvent => "www.roblox.com/robloxStudioStartedEvent";
public override string FinalDirectory => Path.Combine(Paths.Roblox, "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

@ -34,57 +34,18 @@ namespace Bloxstrap
private readonly CancellationTokenSource _cancelTokenSource = new(); private readonly CancellationTokenSource _cancelTokenSource = new();
private bool FreshInstall => String.IsNullOrEmpty(_versionGuid);
private IAppData AppData; private IAppData AppData;
private string _playerLocation => Path.Combine(_versionFolder, AppData.ExecutableName); private bool FreshInstall => String.IsNullOrEmpty(AppData.State.VersionGuid);
private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs;
private LaunchMode _launchMode = App.LaunchSettings.RobloxLaunchMode; private LaunchMode _launchMode = App.LaunchSettings.RobloxLaunchMode;
private bool _installWebView2;
private string _versionGuid
{
get
{
return _launchMode == LaunchMode.Player ? App.State.Prop.PlayerVersionGuid : App.State.Prop.StudioVersionGuid;
}
set
{
if (_launchMode == LaunchMode.Player)
App.State.Prop.PlayerVersionGuid = value;
else
App.State.Prop.StudioVersionGuid = value;
}
}
private int _distributionSize
{
get
{
return _launchMode == LaunchMode.Player ? App.State.Prop.PlayerSize : App.State.Prop.StudioSize;
}
set
{
if (_launchMode == LaunchMode.Player)
App.State.Prop.PlayerSize = value;
else
App.State.Prop.StudioSize = value;
}
}
private string _latestVersionGuid = null!; private string _latestVersionGuid = null!;
private PackageManifest _versionPackageManifest = null!; private PackageManifest _versionPackageManifest = null!;
private string _versionFolder = null!;
private bool _isInstalling = false; private bool _isInstalling = false;
private double _progressIncrement; private double _progressIncrement;
private long _totalDownloadedBytes = 0; private long _totalDownloadedBytes = 0;
private int _packagesExtracted = 0;
private bool _cancelFired = false;
public IBootstrapperDialog? Dialog = null; public IBootstrapperDialog? Dialog = null;
@ -92,14 +53,9 @@ namespace Bloxstrap
#endregion #endregion
#region Core #region Core
public Bootstrapper(bool installWebView2) public Bootstrapper()
{ {
_installWebView2 = installWebView2; AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData();
if (_launchMode == LaunchMode.Player)
AppData = new RobloxPlayerData();
else
AppData = new RobloxStudioData();
} }
private void SetStatus(string message) private void SetStatus(string message)
@ -140,6 +96,8 @@ namespace Bloxstrap
var connectionResult = await RobloxDeployment.InitializeConnectivity(); var connectionResult = await RobloxDeployment.InitializeConnectivity();
App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished");
if (connectionResult is not null) if (connectionResult is not null)
{ {
App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed!"); App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed!");
@ -154,6 +112,7 @@ namespace Bloxstrap
else if (connectionResult.GetType() == typeof(AggregateException)) else if (connectionResult.GetType() == typeof(AggregateException))
connectionResult = connectionResult.InnerException!; connectionResult = connectionResult.InnerException!;
// TODO: handle update skip
Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, connectionResult); Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, connectionResult);
App.Terminate(ErrorCode.ERROR_CANCELLED); App.Terminate(ErrorCode.ERROR_CANCELLED);
@ -161,10 +120,6 @@ namespace Bloxstrap
return; return;
} }
App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished");
await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel);
#if !DEBUG || DEBUG_UPDATER #if !DEBUG || DEBUG_UPDATER
if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active) if (App.Settings.Prop.CheckForUpdates && !App.LaunchSettings.UpgradeFlag.Active)
{ {
@ -182,8 +137,8 @@ namespace Bloxstrap
try try
{ {
Mutex.OpenExisting("Bloxstrap_SingletonMutex").Close(); Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close();
App.Logger.WriteLine(LOG_IDENT, "Bloxstrap_SingletonMutex mutex exists, waiting..."); App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting...");
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
mutexExists = true; mutexExists = true;
} }
@ -193,7 +148,7 @@ namespace Bloxstrap
} }
// wait for mutex to be released if it's not yet // wait for mutex to be released if it's not yet
await using var mutex = new AsyncMutex(true, "Bloxstrap_SingletonMutex"); await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper");
await mutex.AcquireAsync(_cancelTokenSource.Token); await mutex.AcquireAsync(_cancelTokenSource.Token);
// reload our configs since they've likely changed by now // reload our configs since they've likely changed by now
@ -203,37 +158,48 @@ namespace Bloxstrap
App.State.Load(); App.State.Load();
} }
await CheckLatestVersion(); // TODO: handle exception and update skip
await GetLatestVersionInfo();
// install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist
if (_latestVersionGuid != _versionGuid || !File.Exists(_playerLocation)) if (!File.Exists(AppData.ExecutablePath) || AppData.State.VersionGuid != _latestVersionGuid)
await InstallLatestVersion(); await InstallLatestVersion();
if (_installWebView2) //await ApplyModifications();
await InstallWebView2();
await ApplyModifications(); // check if launch uri is set to our bootstrapper
// this doesn't go under register, so we check every launch
// just in case the stock bootstrapper changes it back
// TODO: move this to install/upgrade flow if (IsStudioLaunch)
if (FreshInstall) {
RegisterProgramSize(); #if STUDIO_FEATURES
ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application);
ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application);
CheckInstall(); ProtocolHandler.RegisterRobloxPlace(Paths.Application);
ProtocolHandler.RegisterExtension(".rbxl");
// at this point we've finished updating our configs ProtocolHandler.RegisterExtension(".rbxlx");
App.State.Save(); #endif
}
else
{
// TODO: there needs to be better helper functions for these
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\"");
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
}
await mutex.ReleaseAsync(); await mutex.ReleaseAsync();
if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelFired) if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested)
StartRoblox(); StartRoblox();
Dialog?.CloseBootstrapper(); Dialog?.CloseBootstrapper();
} }
private async Task CheckLatestVersion() private async Task GetLatestVersionInfo()
{ {
const string LOG_IDENT = "Bootstrapper::CheckLatestVersion"; const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo";
// before we do anything, we need to query our channel // 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 // if it's set in the launch uri, we need to use it and set the registry key for it
@ -249,7 +215,7 @@ namespace Bloxstrap
{ {
channel = match.Groups[1].Value.ToLowerInvariant(); channel = match.Groups[1].Value.ToLowerInvariant();
} }
else if (key.GetValue("www.roblox.com") is string value) else if (key.GetValue("www.roblox.com") is string value && !String.IsNullOrEmpty(value))
{ {
channel = value; channel = value;
} }
@ -285,8 +251,11 @@ namespace Bloxstrap
key.SetValue("www.roblox.com", channel); key.SetValue("www.roblox.com", channel);
_latestVersionGuid = clientVersion.VersionGuid; _latestVersionGuid = clientVersion.VersionGuid;
_versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid);
_versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); string pkgManifestUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt");
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
_versionPackageManifest = new(pkgManifestData);
} }
private void StartRoblox() private void StartRoblox()
@ -313,9 +282,9 @@ namespace Bloxstrap
var startInfo = new ProcessStartInfo() var startInfo = new ProcessStartInfo()
{ {
FileName = _playerLocation, FileName = AppData.ExecutablePath,
Arguments = _launchCommandLine, Arguments = _launchCommandLine,
WorkingDirectory = _versionFolder WorkingDirectory = AppData.FinalDirectory
}; };
if (_launchMode == LaunchMode.StudioAuth) if (_launchMode == LaunchMode.StudioAuth)
@ -340,7 +309,7 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid}), waiting for start event"); App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid}), waiting for start event");
startEventSignalled = startEvent.WaitOne(TimeSpan.FromSeconds(10)); startEventSignalled = startEvent.WaitOne(TimeSpan.FromSeconds(30));
} }
if (!startEventSignalled) if (!startEventSignalled)
@ -351,6 +320,9 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Start event signalled"); App.Logger.WriteLine(LOG_IDENT, "Start event signalled");
if (IsStudioLaunch)
return;
var autoclosePids = new List<int>(); var autoclosePids = new List<int>();
// launch custom integrations now // launch custom integrations now
@ -401,23 +373,23 @@ namespace Bloxstrap
if (!_isInstalling) if (!_isInstalling)
{ {
// TODO: this sucks and needs to be done better
App.Terminate(ErrorCode.ERROR_CANCELLED); App.Terminate(ErrorCode.ERROR_CANCELLED);
return; return;
} }
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
return; return;
App.Logger.WriteLine(LOG_IDENT, "Cancelling install..."); App.Logger.WriteLine(LOG_IDENT, "Cancelling install...");
_cancelTokenSource.Cancel(); _cancelTokenSource.Cancel();
_cancelFired = true;
try try
{ {
// clean up install // clean up install
if (Directory.Exists(_versionFolder)) if (Directory.Exists(AppData.StagingDirectory))
Directory.Delete(_versionFolder, true); Directory.Delete(AppData.StagingDirectory, true);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -432,47 +404,6 @@ namespace Bloxstrap
#endregion #endregion
#region App Install #region App Install
public void RegisterProgramSize()
{
const string LOG_IDENT = "Bootstrapper::RegisterProgramSize";
App.Logger.WriteLine(LOG_IDENT, "Registering approximate program size...");
using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}");
// sum compressed and uncompressed package sizes and convert to kilobytes
int distributionSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000;
_distributionSize = distributionSize;
int totalSize = App.State.Prop.PlayerSize + App.State.Prop.StudioSize;
uninstallKey.SetValue("EstimatedSize", totalSize);
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
}
public static void CheckInstall()
{
const string LOG_IDENT = "Bootstrapper::CheckInstall";
App.Logger.WriteLine(LOG_IDENT, "Checking install");
// check if launch uri is set to our bootstrapper
// this doesn't go under register, so we check every launch
// just in case the stock bootstrapper changes it back
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\"");
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
#if STUDIO_FEATURES
ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application);
ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application);
ProtocolHandler.RegisterRobloxPlace(Paths.Application);
ProtocolHandler.RegisterExtension(".rbxl");
ProtocolHandler.RegisterExtension(".rbxlx");
#endif
}
private async Task<bool> CheckForUpdates() private async Task<bool> CheckForUpdates()
{ {
const string LOG_IDENT = "Bootstrapper::CheckForUpdates"; const string LOG_IDENT = "Bootstrapper::CheckForUpdates";
@ -588,26 +519,28 @@ namespace Bloxstrap
Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Base);
Directory.CreateDirectory(Paths.Downloads); Directory.CreateDirectory(Paths.Downloads);
Directory.CreateDirectory(Paths.Versions); Directory.CreateDirectory(Paths.Roblox);
if (Directory.Exists(AppData.StagingDirectory))
Directory.Delete(AppData.StagingDirectory, true);
Directory.CreateDirectory(AppData.StagingDirectory);
// package manifest states packed size and uncompressed size in exact bytes // package manifest states packed size and uncompressed size in exact bytes
// packed size only matters if we don't already have the package cached on disk // packed size only matters if we don't already have the package cached on disk
string[] cachedPackages = Directory.GetFiles(Paths.Downloads); var cachedPackages = Directory.GetFiles(Paths.Downloads);
int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size); int totalSizeRequired = _versionPackageManifest.Where(x => !cachedPackages.Contains(x.Signature)).Sum(x => x.PackedSize) + _versionPackageManifest.Sum(x => x.Size);
if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired)
{ {
Frontend.ShowMessageBox( Frontend.ShowMessageBox(Strings.Bootstrapper_NotEnoughSpace, MessageBoxImage.Error);
Strings.Bootstrapper_NotEnoughSpace,
MessageBoxImage.Error
);
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
return; return;
} }
if (Dialog is not null) if (Dialog is not null)
{ {
// TODO: cancelling needs to always be enabled
Dialog.CancelEnabled = true; Dialog.CancelEnabled = true;
Dialog.ProgressStyle = ProgressBarStyle.Continuous; Dialog.ProgressStyle = ProgressBarStyle.Continuous;
@ -617,9 +550,11 @@ namespace Bloxstrap
_progressIncrement = (double)ProgressBarMaximum / _versionPackageManifest.Sum(package => package.PackedSize); _progressIncrement = (double)ProgressBarMaximum / _versionPackageManifest.Sum(package => package.PackedSize);
} }
foreach (Package package in _versionPackageManifest) var extractionTasks = new List<Task>();
foreach (var package in _versionPackageManifest)
{ {
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
return; return;
// download all the packages synchronously // download all the packages synchronously
@ -631,40 +566,91 @@ namespace Bloxstrap
// extract the package immediately after download asynchronously // extract the package immediately after download asynchronously
// discard is just used to suppress the warning // discard is just used to suppress the warning
_ = Task.Run(() => ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}")); extractionTasks.Add(Task.Run(() => ExtractPackage(package), _cancelTokenSource.Token));
} }
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
return; return;
// allow progress bar to 100% before continuing (purely ux reasons lol)
await Task.Delay(1000);
if (Dialog is not null) if (Dialog is not null)
{ {
// allow progress bar to 100% before continuing (purely ux reasons lol)
// TODO: come up with a better way of handling this that is non-blocking
await Task.Delay(1000);
Dialog.ProgressStyle = ProgressBarStyle.Marquee; Dialog.ProgressStyle = ProgressBarStyle.Marquee;
SetStatus(Strings.Bootstrapper_Status_Configuring); SetStatus(Strings.Bootstrapper_Status_Configuring);
} }
// wait for all packages to finish extracting, with an exception for the webview2 runtime installer // TODO: handle faulted tasks
while (_packagesExtracted < _versionPackageManifest.Where(x => x.Name != "WebView2RuntimeInstaller.zip").Count()) await Task.WhenAll(extractionTasks);
{
await Task.Delay(100);
}
App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml..."); App.Logger.WriteLine(LOG_IDENT, "Writing AppSettings.xml...");
string appSettingsLocation = Path.Combine(_versionFolder, "AppSettings.xml"); await File.WriteAllTextAsync(Path.Combine(AppData.StagingDirectory, "AppSettings.xml"), AppSettings);
await File.WriteAllTextAsync(appSettingsLocation, AppSettings);
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
return; return;
if (!FreshInstall) if (FreshInstall)
{ {
// let's take this opportunity to delete any packages we don't need anymore 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 null && hkcuKey is null)
{
var result = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes);
if (result == MessageBoxResult.Yes)
{
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(AppData.StagingDirectory, 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
AppData.State.VersionGuid = _latestVersionGuid;
AppData.State.PackageHashes.Clear();
foreach (var package in _versionPackageManifest)
AppData.State.PackageHashes.Add(package.Name, package.Signature);
var allPackageHashes = new List<string>();
allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values);
allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values);
foreach (string filename in cachedPackages) foreach (string filename in cachedPackages)
{ {
if (!_versionPackageManifest.Exists(package => filename.Contains(package.Signature))) if (!allPackageHashes.Contains(filename))
{ {
App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}"); App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {filename}");
@ -680,284 +666,199 @@ namespace Bloxstrap
} }
} }
string oldVersionFolder = Path.Combine(Paths.Versions, _versionGuid); App.Logger.WriteLine(LOG_IDENT, "Registering approximate program size...");
// move old compatibility flags for the old location int distributionSize = _versionPackageManifest.Sum(x => x.Size + x.PackedSize) / 1000;
using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"))
AppData.State.Size = distributionSize;
int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size;
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{ {
string oldGameClientLocation = Path.Combine(oldVersionFolder, AppData.ExecutableName); uninstallKey.SetValue("EstimatedSize", totalSize);
string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation);
if (appFlags is not null)
{
App.Logger.WriteLine(LOG_IDENT, $"Migrating app compatibility flags from {oldGameClientLocation} to {_playerLocation}...");
appFlagsKey.SetValue(_playerLocation, appFlags);
appFlagsKey.DeleteValue(oldGameClientLocation);
}
}
} }
_versionGuid = _latestVersionGuid; App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
// delete any old version folders
// we only do this if roblox isnt running just in case an update happened
// while they were launching a second instance or something idk
#if STUDIO_FEATURES
if (!Process.GetProcessesByName(App.RobloxPlayerAppName).Any() && !Process.GetProcessesByName(App.RobloxStudioAppName).Any())
#else
if (!Process.GetProcessesByName(App.RobloxPlayerAppName).Any())
#endif
{
foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories())
{
if (dir.Name == App.State.Prop.PlayerVersionGuid || dir.Name == App.State.Prop.StudioVersionGuid || !dir.Name.StartsWith("version-"))
continue;
App.Logger.WriteLine(LOG_IDENT, $"Removing old version folder for {dir.Name}");
try
{
dir.Delete(true);
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!");
App.Logger.WriteException(LOG_IDENT, ex);
}
}
}
// don't register program size until the program is registered, which will be done after this
if (!FreshInstall)
RegisterProgramSize();
if (Dialog is not null) if (Dialog is not null)
Dialog.CancelEnabled = false; Dialog.CancelEnabled = false;
if (Directory.Exists(AppData.FinalDirectory))
{
try
{
// gross hack to see if roblox is still running
// i don't want to rely on mutexes because they can change, and will false flag for
// running installations that are not by bloxstrap
File.Delete(AppData.ExecutablePath);
Directory.Delete(AppData.FinalDirectory, true);
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Could not delete executable/folder, Roblox may still be running. Aborting update.");
App.Logger.WriteException(LOG_IDENT, ex);
Directory.Delete(AppData.StagingDirectory);
_isInstalling = false;
return;
}
}
Directory.Move(AppData.StagingDirectory, AppData.FinalDirectory);
App.State.Save();
_isInstalling = false; _isInstalling = false;
} }
private async Task InstallWebView2() //private async Task ApplyModifications()
{ //{
const string LOG_IDENT = "Bootstrapper::InstallWebView2"; // const string LOG_IDENT = "Bootstrapper::ApplyModifications";
App.Logger.WriteLine(LOG_IDENT, "Installing runtime..."); // if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any())
// {
// App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check");
// return;
// }
string baseDirectory = Path.Combine(_versionFolder, "WebView2RuntimeInstaller"); // SetStatus(Strings.Bootstrapper_Status_ApplyingModifications);
if (!Directory.Exists(baseDirectory)) // // handle file mods
{ // App.Logger.WriteLine(LOG_IDENT, "Checking file mods...");
Package? package = _versionPackageManifest.Find(x => x.Name == "WebView2RuntimeInstaller.zip");
if (package is null) // // manifest has been moved to State.json
{ // File.Delete(Path.Combine(Paths.Base, "ModManifest.txt"));
App.Logger.WriteLine(LOG_IDENT, "Aborted runtime install because package does not exist, has WebView2 been added in this Roblox version yet?");
return;
}
await ExtractPackage(package); // List<string> modFolderFiles = new();
}
SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); // if (!Directory.Exists(Paths.Modifications))
// Directory.CreateDirectory(Paths.Modifications);
ProcessStartInfo startInfo = new() // // check custom font mod
{ // // instead of replacing the fonts themselves, we'll just alter the font family manifests
WorkingDirectory = baseDirectory,
FileName = Path.Combine(baseDirectory, "MicrosoftEdgeWebview2Setup.exe"),
Arguments = "/silent /install"
};
await Process.Start(startInfo)!.WaitForExitAsync(); // string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families");
App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); // if (File.Exists(Paths.CustomFont))
} // {
// App.Logger.WriteLine(LOG_IDENT, "Begin font check");
private async Task ApplyModifications() // Directory.CreateDirectory(modFontFamiliesFolder);
{
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) // foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families")))
{ // {
App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); // string jsonFilename = Path.GetFileName(jsonFilePath);
return; // string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename);
}
SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); // if (File.Exists(modFilepath))
// continue;
// set executable flags for fullscreen optimizations // App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}");
App.Logger.WriteLine(LOG_IDENT, "Checking executable flags...");
using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"))
{
string flag = " DISABLEDXMAXIMIZEDWINDOWEDMODE";
string? appFlags = (string?)appFlagsKey.GetValue(_playerLocation);
if (App.Settings.Prop.DisableFullscreenOptimizations) // FontFamily? fontFamilyData = JsonSerializer.Deserialize<FontFamily>(File.ReadAllText(jsonFilePath));
{
if (appFlags is null)
appFlagsKey.SetValue(_playerLocation, $"~{flag}");
else if (!appFlags.Contains(flag))
appFlagsKey.SetValue(_playerLocation, appFlags + flag);
}
else if (appFlags is not null && appFlags.Contains(flag))
{
App.Logger.WriteLine(LOG_IDENT, $"Deleting flag '{flag.Trim()}'");
// if there's more than one space, there's more flags set we need to preserve // if (fontFamilyData is null)
if (appFlags.Split(' ').Length > 2) // continue;
appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length));
else
appFlagsKey.DeleteValue(_playerLocation);
}
// hmm, maybe make a unified handler for this? this is just lazily copy pasted from above // foreach (FontFace fontFace in fontFamilyData.Faces)
// fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf";
flag = " RUNASADMIN"; // // TODO: writing on every launch is not necessary
appFlags = (string?)appFlagsKey.GetValue(_playerLocation); // File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true }));
// }
if (appFlags is not null && appFlags.Contains(flag)) // App.Logger.WriteLine(LOG_IDENT, "End font check");
{ // }
App.Logger.WriteLine(LOG_IDENT, $"Deleting flag '{flag.Trim()}'"); // else if (Directory.Exists(modFontFamiliesFolder))
// {
// Directory.Delete(modFontFamiliesFolder, true);
// }
// if there's more than one space, there's more flags set we need to preserve // foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories))
if (appFlags.Split(' ').Length > 2) // {
appFlagsKey.SetValue(_playerLocation, appFlags.Remove(appFlags.IndexOf(flag), flag.Length)); // // get relative directory path
else // string relativeFile = file.Substring(Paths.Modifications.Length + 1);
appFlagsKey.DeleteValue(_playerLocation);
}
}
// handle file mods // // v1.7.0 - README has been moved to the preferences menu now
App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); // if (relativeFile == "README.txt")
// {
// File.Delete(file);
// continue;
// }
// manifest has been moved to State.json // if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase))
File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); // continue;
List<string> modFolderFiles = new(); // if (relativeFile.EndsWith(".lock"))
// continue;
if (!Directory.Exists(Paths.Modifications)) // modFolderFiles.Add(relativeFile);
Directory.CreateDirectory(Paths.Modifications);
// check custom font mod // string fileModFolder = Path.Combine(Paths.Modifications, relativeFile);
// instead of replacing the fonts themselves, we'll just alter the font family manifests // string fileVersionFolder = Path.Combine(_versionFolder, relativeFile);
string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); // 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;
// }
if (File.Exists(Paths.CustomFont)) // Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
{
App.Logger.WriteLine(LOG_IDENT, "Begin font check");
Directory.CreateDirectory(modFontFamiliesFolder); // Filesystem.AssertReadOnly(fileVersionFolder);
// File.Copy(fileModFolder, fileVersionFolder, true);
// Filesystem.AssertReadOnly(fileVersionFolder);
foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families"))) // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder");
{ // }
string jsonFilename = Path.GetFileName(jsonFilePath);
string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename);
if (File.Exists(modFilepath)) // // the manifest is primarily here to keep track of what files have been
continue; // // 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
App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); // // TODO: this needs to extract the files from packages in bulk, this is way too slow
// foreach (string fileLocation in App.State.Prop.ModManifest)
// {
// if (modFolderFiles.Contains(fileLocation))
// continue;
FontFamily? fontFamilyData = JsonSerializer.Deserialize<FontFamily>(File.ReadAllText(jsonFilePath)); // var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value));
if (fontFamilyData is null) // // package doesn't exist, likely mistakenly placed file
continue; // if (String.IsNullOrEmpty(package.Key))
// {
// App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package");
foreach (FontFace fontFace in fontFamilyData.Faces) // string versionFileLocation = Path.Combine(_versionFolder, fileLocation);
fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf";
// TODO: writing on every launch is not necessary // if (File.Exists(versionFileLocation))
File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); // File.Delete(versionFileLocation);
}
App.Logger.WriteLine(LOG_IDENT, "End font check"); // continue;
} // }
else if (Directory.Exists(modFontFamiliesFolder))
{
Directory.Delete(modFontFamiliesFolder, true);
}
foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) // // restore original file
{ // string fileName = fileLocation.Substring(package.Value.Length);
// get relative directory path // await ExtractFileFromPackage(package.Key, fileName);
string relativeFile = file.Substring(Paths.Modifications.Length + 1);
// v1.7.0 - README has been moved to the preferences menu now // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}");
if (relativeFile == "README.txt") // }
{
File.Delete(file);
continue;
}
if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) // App.State.Prop.ModManifest = modFolderFiles;
continue; // App.State.Save();
if (relativeFile.EndsWith(".lock")) // App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
continue; //}
modFolderFiles.Add(relativeFile);
string fileModFolder = Path.Combine(Paths.Modifications, relativeFile);
string fileVersionFolder = Path.Combine(_versionFolder, 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
// TODO: this needs to extract the files from packages in bulk, this is way too slow
foreach (string fileLocation in App.State.Prop.ModManifest)
{
if (modFolderFiles.Contains(fileLocation))
continue;
var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value));
// package doesn't exist, likely mistakenly placed file
if (String.IsNullOrEmpty(package.Key))
{
App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package");
string versionFileLocation = Path.Combine(_versionFolder, fileLocation);
if (File.Exists(versionFileLocation))
File.Delete(versionFileLocation);
continue;
}
// restore original file
string fileName = fileLocation.Substring(package.Value.Length);
await ExtractFileFromPackage(package.Key, fileName);
App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}");
}
App.State.Prop.ModManifest = modFolderFiles;
App.State.Save();
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");
}
private async Task DownloadPackage(Package package) private async Task DownloadPackage(Package package)
{ {
string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}"; string LOG_IDENT = $"Bootstrapper::DownloadPackage.{package.Name}";
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
return; return;
string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}");
@ -966,7 +867,7 @@ namespace Bloxstrap
if (File.Exists(packageLocation)) if (File.Exists(packageLocation))
{ {
FileInfo file = new(packageLocation); var file = new FileInfo(packageLocation);
string calculatedMD5 = MD5Hash.FromFile(packageLocation); string calculatedMD5 = MD5Hash.FromFile(packageLocation);
@ -1002,6 +903,9 @@ namespace Bloxstrap
if (File.Exists(packageLocation)) if (File.Exists(packageLocation))
return; return;
// TODO: telemetry for this. chances are that this is completely unnecessary and that it can be removed.
// but, we need to ensure this doesn't work before we can do that.
const int maxTries = 5; const int maxTries = 5;
App.Logger.WriteLine(LOG_IDENT, "Downloading..."); App.Logger.WriteLine(LOG_IDENT, "Downloading...");
@ -1010,7 +914,7 @@ namespace Bloxstrap
for (int i = 1; i <= maxTries; i++) for (int i = 1; i <= maxTries; i++)
{ {
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
return; return;
int totalBytesRead = 0; int totalBytesRead = 0;
@ -1023,7 +927,7 @@ namespace Bloxstrap
while (true) while (true)
{ {
if (_cancelFired) if (_cancelTokenSource.IsCancellationRequested)
{ {
stream.Close(); stream.Close();
fileStream.Close(); fileStream.Close();
@ -1087,15 +991,12 @@ namespace Bloxstrap
} }
} }
private Task ExtractPackage(Package package) private void ExtractPackage(Package package)
{ {
const string LOG_IDENT = "Bootstrapper::ExtractPackage"; const string LOG_IDENT = "Bootstrapper::ExtractPackage";
if (_cancelFired)
return Task.CompletedTask;
string packageLocation = Path.Combine(Paths.Downloads, package.Signature); string packageLocation = Path.Combine(Paths.Downloads, package.Signature);
string packageFolder = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name]); string packageFolder = Path.Combine(AppData.StagingDirectory, AppData.PackageDirectoryMap[package.Name]);
App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}...");
@ -1103,31 +1004,27 @@ namespace Bloxstrap
fastZip.ExtractZip(packageLocation, packageFolder, null); fastZip.ExtractZip(packageLocation, packageFolder, null);
App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}");
_packagesExtracted += 1;
return Task.CompletedTask;
} }
private async Task ExtractFileFromPackage(string packageName, string fileName) //private async Task ExtractFileFromPackage(string packageName, string fileName)
{ //{
Package? package = _versionPackageManifest.Find(x => x.Name == packageName); // Package? package = _versionPackageManifest.Find(x => x.Name == packageName);
if (package is null) // if (package is null)
return; // return;
await DownloadPackage(package); // await DownloadPackage(package);
using ZipArchive archive = ZipFile.OpenRead(Path.Combine(Paths.Downloads, package.Signature)); // using ZipArchive archive = ZipFile.OpenRead(Path.Combine(Paths.Downloads, package.Signature));
ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); // ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName);
if (entry is null) // if (entry is null)
return; // return;
string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName); // string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName);
entry.ExtractToFile(extractionPath, true); // entry.ExtractToFile(extractionPath, true);
} //}
#endregion #endregion
} }
} }

View File

@ -1,6 +1,4 @@
using System.Windows; namespace Bloxstrap.Integrations
namespace Bloxstrap.Integrations
{ {
public class ActivityWatcher : IDisposable public class ActivityWatcher : IDisposable
{ {

View File

@ -169,15 +169,6 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND); App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
} }
bool installWebView2 = false;
{
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 null && hkcuKey is null)
installWebView2 = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes) == MessageBoxResult.Yes;
}
if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _)) if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
{ {
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex // this currently doesn't work very well since it relies on checking the existence of the singleton mutex
@ -195,7 +186,7 @@ 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(installWebView2); var bootstrapper = new Bootstrapper();
IBootstrapperDialog? dialog = null; IBootstrapperDialog? dialog = null;
if (!App.LaunchSettings.QuietFlag.Active) if (!App.LaunchSettings.QuietFlag.Active)

View File

@ -137,7 +137,7 @@ namespace Bloxstrap.Models
private void RejoinServer() private void RejoinServer()
{ {
string playerPath = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe"); string playerPath = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe");
Process.Start(playerPath, GetInviteDeeplink(false)); Process.Start(playerPath, GetInviteDeeplink(false));
} }

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models
{
public class AppState
{
public string VersionGuid { get; set; } = String.Empty;
public Dictionary<string, string> PackageHashes { get; set; } = new();
public int Size { get; set; }
}
}

View File

@ -8,9 +8,9 @@ namespace Bloxstrap.Models.Manifest
{ {
public class PackageManifest : List<Package> public class PackageManifest : List<Package>
{ {
private PackageManifest(string data) public PackageManifest(string data)
{ {
using StringReader reader = new StringReader(data); using var reader = new StringReader(data);
string? version = reader.ReadLine(); string? version = reader.ReadLine();
if (version != "v0") if (version != "v0")
@ -46,13 +46,5 @@ namespace Bloxstrap.Models.Manifest
}); });
} }
} }
public static async Task<PackageManifest> Get(string versionGuid)
{
string pkgManifestUrl = RobloxDeployment.GetLocation($"/{versionGuid}-rbxPkgManifest.txt");
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
return new PackageManifest(pkgManifestData);
}
} }
} }

View File

@ -4,13 +4,9 @@
{ {
public bool ShowFFlagEditorWarning { get; set; } = true; public bool ShowFFlagEditorWarning { get; set; } = true;
[Obsolete("Use PlayerVersionGuid instead", true)] public AppState Player { get; set; } = new();
public string VersionGuid { set { PlayerVersionGuid = value; } }
public string PlayerVersionGuid { get; set; } = "";
public string StudioVersionGuid { get; set; } = "";
public int PlayerSize { get; set; } = 0; public AppState Studio { get; set; } = new();
public int StudioSize { get; set; } = 0;
public List<string> ModManifest { get; set; } = new(); public List<string> ModManifest { get; set; } = new();
} }

View File

@ -22,6 +22,7 @@
public static string Integrations { get; private set; } = ""; public static string Integrations { get; private set; } = "";
public static string Versions { get; private set; } = ""; public static string Versions { get; private set; } = "";
public static string Modifications { get; private set; } = ""; public static string Modifications { get; private set; } = "";
public static string Roblox { get; private set; } = "";
public static string Application { get; private set; } = ""; public static string Application { get; private set; } = "";
@ -37,6 +38,7 @@
Integrations = Path.Combine(Base, "Integrations"); Integrations = Path.Combine(Base, "Integrations");
Versions = Path.Combine(Base, "Versions"); Versions = Path.Combine(Base, "Versions");
Modifications = Path.Combine(Base, "Modifications"); Modifications = Path.Combine(Base, "Modifications");
Roblox = Path.Combine(Base, "Roblox");
Application = Path.Combine(Base, $"{App.ProjectName}.exe"); Application = Path.Combine(Base, $"{App.ProjectName}.exe");
} }

View File

@ -15,6 +15,7 @@
private static readonly Dictionary<string, int> BaseUrls = new() private static readonly Dictionary<string, int> BaseUrls = new()
{ {
{ "https://setup.rbxcdn.com", 0 }, { "https://setup.rbxcdn.com", 0 },
{ "https://setup-aws.rbxcdn.com", 2 },
{ "https://setup-ak.rbxcdn.com", 2 }, { "https://setup-ak.rbxcdn.com", 2 },
{ "https://roblox-setup.cachefly.net", 2 }, { "https://roblox-setup.cachefly.net", 2 },
{ "https://s3.amazonaws.com/setup.roblox.com", 4 } { "https://s3.amazonaws.com/setup.roblox.com", 4 }
@ -22,7 +23,7 @@
private static async Task<string?> TestConnection(string url, int priority, CancellationToken token) private static async Task<string?> TestConnection(string url, int priority, CancellationToken token)
{ {
string LOG_IDENT = $"RobloxDeployment::TestConnection.{url}"; string LOG_IDENT = $"RobloxDeployment::TestConnection<{url}>";
await Task.Delay(priority * 1000, token); await Task.Delay(priority * 1000, token);
@ -38,8 +39,9 @@
// versionStudio is the version hash for the last MFC studio to be deployed. // versionStudio is the version hash for the last MFC studio to be deployed.
// the response body should always be "version-012732894899482c". // the response body should always be "version-012732894899482c".
string content = await response.Content.ReadAsStringAsync(token); string content = await response.Content.ReadAsStringAsync(token);
if (content != VersionStudioHash) if (content != VersionStudioHash)
throw new Exception($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")"); throw new InvalidHTTPResponseException($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")");
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
@ -66,11 +68,10 @@
// returns null for success // returns null for success
CancellationTokenSource tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
var exceptions = new List<Exception>(); var exceptions = new List<Exception>();
var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, token)).ToList(); var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, tokenSource.Token)).ToList();
App.Logger.WriteLine(LOG_IDENT, "Testing connectivity..."); App.Logger.WriteLine(LOG_IDENT, "Testing connectivity...");
@ -127,7 +128,11 @@
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel}"); App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel}");
if (String.IsNullOrEmpty(channel))
channel = DefaultChannel;
string cacheKey = $"{channel}-{binaryType}"; string cacheKey = $"{channel}-{binaryType}";
ClientVersion clientVersion; ClientVersion clientVersion;
if (ClientVersionCache.ContainsKey(cacheKey)) if (ClientVersionCache.ContainsKey(cacheKey))
@ -137,48 +142,27 @@
} }
else else
{ {
bool isDefaultChannel = String.Compare(channel, DefaultChannel, StringComparison.OrdinalIgnoreCase) == 0;
string path = $"/v2/client-version/{binaryType}"; string path = $"/v2/client-version/{binaryType}";
if (String.Compare(channel, DefaultChannel, StringComparison.InvariantCultureIgnoreCase) != 0) if (!isDefaultChannel)
path = $"/v2/client-version/{binaryType}/channel/{channel}"; path = $"/v2/client-version/{binaryType}/channel/{channel}";
HttpResponseMessage deployInfoResponse;
try try
{ {
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path); clientVersion = await Http.GetJson<ClientVersion>($"https://clientsettingscdn.roblox.com/{path}");
} }
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...");
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettings.roblox.com" + path); clientVersion = await Http.GetJson<ClientVersion>($"https://clientsettings.roblox.com/{path}");
}
string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync();
if (!deployInfoResponse.IsSuccessStatusCode)
{
// 400 = Invalid binaryType.
// 404 = Could not find version details for binaryType.
// 500 = Error while fetching version information.
// either way, we throw
App.Logger.WriteLine(LOG_IDENT,
"Failed to fetch deploy info!\r\n" +
$"\tStatus code: {deployInfoResponse.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseException(deployInfoResponse);
}
clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawResponse)!;
} }
// check if channel is behind LIVE // check if channel is behind LIVE
if (channel != DefaultChannel) if (!isDefaultChannel)
{ {
var defaultClientVersion = await GetInfo(DefaultChannel); var defaultClientVersion = await GetInfo(DefaultChannel);
@ -187,6 +171,7 @@
} }
ClientVersionCache[cacheKey] = clientVersion; ClientVersionCache[cacheKey] = clientVersion;
}
return clientVersion; return clientVersion;
} }

View File

@ -19,8 +19,6 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
public ICommand CopyInstanceIdCommand => new RelayCommand(CopyInstanceId); public ICommand CopyInstanceIdCommand => new RelayCommand(CopyInstanceId);
public EventHandler? RequestCloseEvent;
public ServerInformationViewModel(Watcher watcher) public ServerInformationViewModel(Watcher watcher)
{ {
_activityWatcher = watcher.ActivityWatcher!; _activityWatcher = watcher.ActivityWatcher!;

View File

@ -27,20 +27,20 @@
{ {
// wouldnt it be better to check old version guids? // wouldnt it be better to check old version guids?
// what about fresh installs? // what about fresh installs?
get => String.IsNullOrEmpty(App.State.Prop.PlayerVersionGuid) && String.IsNullOrEmpty(App.State.Prop.StudioVersionGuid); get => String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
set set
{ {
if (value) if (value)
{ {
_oldPlayerVersionGuid = App.State.Prop.PlayerVersionGuid; _oldPlayerVersionGuid = App.State.Prop.Player.VersionGuid;
_oldStudioVersionGuid = App.State.Prop.StudioVersionGuid; _oldStudioVersionGuid = App.State.Prop.Studio.VersionGuid;
App.State.Prop.PlayerVersionGuid = ""; App.State.Prop.Player.VersionGuid = "";
App.State.Prop.StudioVersionGuid = ""; App.State.Prop.Studio.VersionGuid = "";
} }
else else
{ {
App.State.Prop.PlayerVersionGuid = _oldPlayerVersionGuid; App.State.Prop.Player.VersionGuid = _oldPlayerVersionGuid;
App.State.Prop.StudioVersionGuid = _oldStudioVersionGuid; App.State.Prop.Studio.VersionGuid = _oldStudioVersionGuid;
} }
} }
} }

View File

@ -51,7 +51,7 @@ namespace Bloxstrap
public static string GetRobloxVersion(bool studio) public static string GetRobloxVersion(bool studio)
{ {
string versionGuid = studio ? App.State.Prop.StudioVersionGuid : App.State.Prop.PlayerVersionGuid; string versionGuid = studio ? App.State.Prop.Studio.VersionGuid : App.State.Prop.Player.VersionGuid;
string fileName = studio ? "RobloxStudioBeta.exe" : "RobloxPlayerBeta.exe"; string fileName = studio ? "RobloxStudioBeta.exe" : "RobloxPlayerBeta.exe";
string playerLocation = Path.Combine(Paths.Versions, versionGuid, fileName); string playerLocation = Path.Combine(Paths.Versions, versionGuid, fileName);

View File

@ -1,20 +0,0 @@
namespace Bloxstrap.Utility
{
public static class AsyncHelpers
{
public static void ExceptionHandler(Task task, object? state)
{
const string LOG_IDENT = "AsyncHelpers::ExceptionHandler";
if (task.Exception is null)
return;
if (state is null)
App.Logger.WriteLine(LOG_IDENT, "An exception occurred while running the task");
else
App.Logger.WriteLine(LOG_IDENT, $"An exception occurred while running the task '{state}'");
App.FinalizeExceptionHandling(task.Exception);
}
}
}

View File

@ -31,7 +31,7 @@ namespace Bloxstrap
#if DEBUG #if DEBUG
if (String.IsNullOrEmpty(watcherData)) if (String.IsNullOrEmpty(watcherData))
{ {
string path = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe"); string path = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe");
using var gameClientProcess = Process.Start(path); using var gameClientProcess = Process.Start(path);
_gameClientPid = gameClientProcess.Id; _gameClientPid = gameClientProcess.Id;
} }