diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs
index 12b62fa..557d0c8 100644
--- a/Bloxstrap/App.xaml.cs
+++ b/Bloxstrap/App.xaml.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using System.Web;
using System.Windows;
using System.Windows.Threading;
@@ -14,7 +15,8 @@ namespace Bloxstrap
{
public const string ProjectName = "Bloxstrap";
public const string ProjectRepository = "pizzaboxer/bloxstrap";
- public const string RobloxAppName = "RobloxPlayerBeta";
+ public const string RobloxPlayerAppName = "RobloxPlayerBeta";
+ public const string RobloxStudioAppName = "RobloxStudioBeta";
// used only for communicating between app and menu - use Directories.Base for anything else
public static string BaseDirectory = null!;
@@ -49,7 +51,9 @@ namespace Bloxstrap
)
);
+#if RELEASE
private static bool _showingExceptionDialog = false;
+#endif
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
@@ -120,6 +124,10 @@ namespace Bloxstrap
LaunchArgs = e.Args;
+#if DEBUG
+ Logger.WriteLine(LOG_IDENT, $"Arguments: {string.Join(' ', LaunchArgs)}");
+#endif
+
HttpClient.Timeout = TimeSpan.FromSeconds(30);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
@@ -189,6 +197,7 @@ namespace Bloxstrap
#endif
string commandLine = "";
+ LaunchMode? launchMode = null;
if (IsMenuLaunch)
{
@@ -216,6 +225,8 @@ namespace Bloxstrap
if (LaunchArgs[0].StartsWith("roblox-player:"))
{
commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]);
+
+ launchMode = LaunchMode.Player;
}
else if (LaunchArgs[0].StartsWith("roblox:"))
{
@@ -226,25 +237,53 @@ namespace Bloxstrap
);
commandLine = $"--app --deeplink {LaunchArgs[0]}";
+
+ launchMode = LaunchMode.Player;
+ }
+ else if (LaunchArgs[0].StartsWith("roblox-studio:"))
+ {
+ commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]);
+
+ if (!commandLine.Contains("-startEvent"))
+ commandLine += " -startEvent www.roblox.com/robloxQTStudioStartedEvent";
+
+ launchMode = LaunchMode.Studio;
+ }
+ else if (LaunchArgs[0].StartsWith("roblox-studio-auth:"))
+ {
+ commandLine = HttpUtility.UrlDecode(LaunchArgs[0]);
+
+ launchMode = LaunchMode.StudioAuth;
+ }
+ else if (LaunchArgs[0] == "-ide")
+ {
+ launchMode = LaunchMode.Studio;
+
+ if (LaunchArgs.Length >= 2)
+ commandLine = $"-task EditFile -localPlaceFile \"{LaunchArgs[1]}\"";
}
else
{
commandLine = "--app";
+
+ launchMode = LaunchMode.Player;
}
}
else
{
commandLine = "--app";
+
+ launchMode = LaunchMode.Player;
}
- if (!String.IsNullOrEmpty(commandLine))
+ if (launchMode != null)
{
if (!IsFirstRun)
ShouldSaveConfigs = true;
// start bootstrapper and show the bootstrapper modal if we're not running silently
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
- Bootstrapper bootstrapper = new(commandLine);
+ Bootstrapper bootstrapper = new(commandLine, (LaunchMode)launchMode);
IBootstrapperDialog? dialog = null;
if (!IsQuiet)
@@ -261,7 +300,7 @@ namespace Bloxstrap
Mutex? singletonMutex = null;
- if (Settings.Prop.MultiInstanceLaunching)
+ if (Settings.Prop.MultiInstanceLaunching && launchMode == LaunchMode.Player)
{
Logger.WriteLine(LOG_IDENT, "Creating singleton mutex");
diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj
index 8ce1388..36a6796 100644
--- a/Bloxstrap/Bloxstrap.csproj
+++ b/Bloxstrap/Bloxstrap.csproj
@@ -45,6 +45,7 @@
all
+
diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs
index d68c50c..c4205f7 100644
--- a/Bloxstrap/Bootstrapper.cs
+++ b/Bloxstrap/Bootstrapper.cs
@@ -10,38 +10,8 @@ namespace Bloxstrap
public class Bootstrapper
{
#region Properties
- // in case a new package is added, you can find the corresponding directory
- // by opening the stock bootstrapper in a hex editor
- // TODO - there ideally should be a less static way to do this that's not hardcoded?
- private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary()
- {
- { "RobloxApp.zip", @"" },
- { "shaders.zip", @"shaders\" },
- { "ssl.zip", @"ssl\" },
-
- // the runtime installer is only extracted if it needs installing
- { "WebView2.zip", @"" },
- { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" },
-
- { "content-avatar.zip", @"content\avatar\" },
- { "content-configs.zip", @"content\configs\" },
- { "content-fonts.zip", @"content\fonts\" },
- { "content-sky.zip", @"content\sky\" },
- { "content-sounds.zip", @"content\sounds\" },
- { "content-textures2.zip", @"content\textures\" },
- { "content-models.zip", @"content\models\" },
-
- { "content-textures3.zip", @"PlatformContent\pc\textures\" },
- { "content-terrain.zip", @"PlatformContent\pc\terrain\" },
- { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" },
-
- { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" },
- { "extracontent-translations.zip", @"ExtraContent\translations\" },
- { "extracontent-models.zip", @"ExtraContent\models\" },
- { "extracontent-textures.zip", @"ExtraContent\textures\" },
- { "extracontent-places.zip", @"ExtraContent\places\" },
- };
-
+ private const int ProgressBarMaximum = 10000;
+
private const string AppSettings =
"\r\n" +
"\r\n" +
@@ -51,15 +21,49 @@ namespace Bloxstrap
private readonly CancellationTokenSource _cancelTokenSource = new();
- private static bool FreshInstall => String.IsNullOrEmpty(App.State.Prop.VersionGuid);
+ private bool FreshInstall => String.IsNullOrEmpty(_versionGuid);
- private string _playerLocation => Path.Combine(_versionFolder, "RobloxPlayerBeta.exe");
+ private string _playerFileName => _launchMode == LaunchMode.Player ? "RobloxPlayerBeta.exe" : "RobloxStudioBeta.exe";
+ // TODO: change name
+ private string _playerLocation => Path.Combine(_versionFolder, _playerFileName);
private string _launchCommandLine;
+ private LaunchMode _launchMode;
+
+ 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 PackageManifest _versionPackageManifest = null!;
- private FileManifest _versionFileManifest = null!;
private string _versionFolder = null!;
private bool _isInstalling = false;
@@ -68,19 +72,33 @@ namespace Bloxstrap
private int _packagesExtracted = 0;
private bool _cancelFired = false;
+ private IReadOnlyDictionary _packageDirectories;
+
public IBootstrapperDialog? Dialog = null;
+
+ public bool IsStudioLaunch => _launchMode != LaunchMode.Player;
#endregion
#region Core
- public Bootstrapper(string launchCommandLine)
+ public Bootstrapper(string launchCommandLine, LaunchMode launchMode)
{
_launchCommandLine = launchCommandLine;
+ _launchMode = launchMode;
+
+ _packageDirectories = _launchMode == LaunchMode.Player ? PackageMap.Player : PackageMap.Studio;
}
private void SetStatus(string message)
{
App.Logger.WriteLine("Bootstrapper::SetStatus", message);
+ string productName = "Roblox";
+
+ if (_launchMode != LaunchMode.Player)
+ productName += " Studio";
+
+ message = message.Replace("{product}", productName);
+
// yea idk
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog)
message = message.Replace("...", "");
@@ -91,15 +109,16 @@ namespace Bloxstrap
private void UpdateProgressBar()
{
- int newProgress = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes);
+ if (Dialog is null)
+ return;
+
+ int progressValue = (int)Math.Floor(_progressIncrement * _totalDownloadedBytes);
// bugcheck: if we're restoring a file from a package, it'll incorrectly increment the progress beyond 100
// too lazy to fix properly so lol
- if (newProgress > 100)
- return;
+ progressValue = Math.Clamp(progressValue, 0, ProgressBarMaximum);
- if (Dialog is not null)
- Dialog.ProgressValue = newProgress;
+ Dialog.ProgressValue = progressValue;
}
public async Task Run()
@@ -183,7 +202,7 @@ namespace Bloxstrap
await CheckLatestVersion();
// install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist
- if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation))
+ if (App.IsFirstRun || _latestVersionGuid != _versionGuid || !File.Exists(_playerLocation))
await InstallLatestVersion();
if (App.IsFirstRun)
@@ -223,18 +242,20 @@ namespace Bloxstrap
ClientVersion clientVersion;
+ string binaryType = _launchMode == LaunchMode.Player ? "WindowsPlayer" : "WindowsStudio64";
+
try
{
- clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel);
+ clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType);
}
catch (HttpResponseException ex)
{
if (ex.ResponseMessage.StatusCode != HttpStatusCode.NotFound)
throw;
- App.Logger.WriteLine(LOG_IDENT, $"Reverting enrolled channel to {RobloxDeployment.DefaultChannel} because a WindowsPlayer build does not exist for {App.Settings.Prop.Channel}");
+ App.Logger.WriteLine(LOG_IDENT, $"Reverting enrolled channel to {RobloxDeployment.DefaultChannel} because a {binaryType} build does not exist for {App.Settings.Prop.Channel}");
App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel;
- clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel);
+ clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType);
}
if (clientVersion.IsBehindDefaultChannel)
@@ -257,21 +278,20 @@ namespace Bloxstrap
App.Logger.WriteLine("Bootstrapper::CheckLatestVersion", $"Changed Roblox channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}");
App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel;
- clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel);
+ clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType);
}
}
_latestVersionGuid = clientVersion.VersionGuid;
_versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid);
_versionPackageManifest = await PackageManifest.Get(_latestVersionGuid);
- _versionFileManifest = await FileManifest.Get(_latestVersionGuid);
}
private async Task StartRoblox()
{
const string LOG_IDENT = "Bootstrapper::StartRoblox";
- SetStatus("Starting Roblox...");
+ SetStatus("Starting {product}...");
if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch)
{
@@ -294,21 +314,38 @@ namespace Bloxstrap
return;
}
- _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString());
+ if (_launchMode != LaunchMode.StudioAuth)
+ {
+ _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString());
- _launchCommandLine += " -channel ";
+ _launchCommandLine += " -channel ";
- if (App.Settings.Prop.Channel.ToLowerInvariant() == RobloxDeployment.DefaultChannel.ToLowerInvariant())
- _launchCommandLine += "production";
- else
- _launchCommandLine += App.Settings.Prop.Channel.ToLowerInvariant();
+ if (App.Settings.Prop.Channel.ToLowerInvariant() == RobloxDeployment.DefaultChannel.ToLowerInvariant())
+ _launchCommandLine += "production";
+ else
+ _launchCommandLine += App.Settings.Prop.Channel.ToLowerInvariant();
+ }
// whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes
bool shouldWait = false;
+ var startInfo = new ProcessStartInfo()
+ {
+ FileName = _playerLocation,
+ Arguments = _launchCommandLine,
+ WorkingDirectory = _versionFolder
+ };
+
+ if (_launchMode == LaunchMode.StudioAuth)
+ {
+ Process.Start(startInfo);
+ Dialog?.CloseBootstrapper();
+ return;
+ }
+
// v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now
int gameClientPid;
- using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine))
+ using (Process gameClient = Process.Start(startInfo)!)
{
gameClientPid = gameClient.Id;
}
@@ -319,7 +356,8 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid})");
- using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent"))
+ string eventName = _launchMode == LaunchMode.Player ? "www.roblox.com/robloxStartedEvent" : "www.roblox.com/robloxQTStudioStartedEvent";
+ using (SystemEvent startEvent = new(eventName))
{
bool startEventFired = await startEvent.WaitForEvent();
@@ -329,6 +367,9 @@ namespace Bloxstrap
return;
}
+ if (App.Settings.Prop.EnableActivityTracking && _launchMode == LaunchMode.Player)
+ App.NotifyIcon?.SetProcessId(gameClientPid);
+
if (App.Settings.Prop.EnableActivityTracking)
{
activityWatcher = new();
@@ -476,7 +517,10 @@ namespace Bloxstrap
using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}");
// sum compressed and uncompressed package sizes and convert to kilobytes
- int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000;
+ 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);
@@ -495,14 +539,26 @@ namespace Bloxstrap
ProtocolHandler.Register("roblox", "Roblox", Paths.Application);
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application);
+ ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application);
+ ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application);
- // in case the user is reinstalling
- if (File.Exists(Paths.Application) && App.IsFirstRun)
- File.Delete(Paths.Application);
+ ProtocolHandler.RegisterRobloxPlace(Paths.Application);
+ ProtocolHandler.RegisterExtension(".rbxl");
+ ProtocolHandler.RegisterExtension(".rbxlx");
- // check to make sure bootstrapper is in the install folder
- if (!File.Exists(Paths.Application) && Environment.ProcessPath is not null)
- File.Copy(Environment.ProcessPath, Paths.Application);
+ if (Environment.ProcessPath is not null && Environment.ProcessPath != Paths.Application)
+ {
+ // in case the user is reinstalling
+ if (File.Exists(Paths.Application) && App.IsFirstRun)
+ {
+ Filesystem.AssertReadOnly(Paths.Application);
+ File.Delete(Paths.Application);
+ }
+
+ // check to make sure bootstrapper is in the install folder
+ if (!File.Exists(Paths.Application))
+ File.Copy(Environment.ProcessPath, Paths.Application);
+ }
// this SHOULD go under Register(),
// but then people who have Bloxstrap v1.0.0 installed won't have this without a reinstall
@@ -522,6 +578,7 @@ namespace Bloxstrap
Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.StartMenu, "Play Roblox.lnk"));
Utility.Shortcut.Create(Paths.Application, "-menu", Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk"));
+ Utility.Shortcut.Create(Paths.Application, "-ide", Path.Combine(Paths.StartMenu, $"Roblox Studio ({App.ProjectName}).lnk"));
if (App.Settings.Prop.CreateDesktopIcon)
{
@@ -630,7 +687,7 @@ namespace Bloxstrap
const string LOG_IDENT = "Bootstrapper::Uninstall";
// prompt to shutdown roblox if its currently running
- if (Process.GetProcessesByName(App.RobloxAppName).Any())
+ if (Process.GetProcessesByName(App.RobloxPlayerAppName).Any() || Process.GetProcessesByName(App.RobloxStudioAppName).Any())
{
App.Logger.WriteLine(LOG_IDENT, $"Prompting to shut down all open Roblox instances");
@@ -645,7 +702,13 @@ namespace Bloxstrap
try
{
- foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta"))
+ foreach (Process process in Process.GetProcessesByName(App.RobloxPlayerAppName))
+ {
+ process.CloseMainWindow();
+ process.Close();
+ }
+
+ foreach (Process process in Process.GetProcessesByName(App.RobloxStudioAppName))
{
process.CloseMainWindow();
process.Close();
@@ -662,16 +725,17 @@ namespace Bloxstrap
SetStatus($"Uninstalling {App.ProjectName}...");
App.ShouldSaveConfigs = false;
- bool robloxStillInstalled = true;
+ bool robloxPlayerStillInstalled = true;
+ bool robloxStudioStillInstalled = true;
// check if stock bootstrapper is still installed
- RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
+ using RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
if (bootstrapperKey is null)
{
+ robloxPlayerStillInstalled = false;
+
ProtocolHandler.Unregister("roblox");
ProtocolHandler.Unregister("roblox-player");
-
- robloxStillInstalled = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio") is not null;
}
else
{
@@ -683,6 +747,27 @@ namespace Bloxstrap
ProtocolHandler.Register("roblox-player", "Roblox", bootstrapperLocation);
}
+ using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
+ if (studioBootstrapperKey is null)
+ {
+ robloxStudioStillInstalled = false;
+
+ ProtocolHandler.Unregister("roblox-studio");
+ ProtocolHandler.Unregister("roblox-studio-auth");
+
+ ProtocolHandler.Unregister("Roblox.Place");
+ ProtocolHandler.Unregister(".rbxl");
+ ProtocolHandler.Unregister(".rbxlx");
+ }
+ else
+ {
+ string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper
+ ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation);
+ ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation);
+
+ ProtocolHandler.RegisterRobloxPlace(studioLocation);
+ }
+
// if the folder we're installed to does not end with "Bloxstrap", we're installed to a user-selected folder
// in which case, chances are they chose to install to somewhere they didn't really mean to (prior to the added warning in 2.4.0)
// if so, we're walking on eggshells and have to ensure we only clean up what we need to clean up
@@ -713,7 +798,7 @@ namespace Bloxstrap
string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox");
- if (!robloxStillInstalled && Directory.Exists(robloxFolder))
+ if (!robloxPlayerStillInstalled && !robloxStudioStillInstalled && Directory.Exists(robloxFolder))
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
foreach (var process in cleanupSequence)
@@ -767,7 +852,7 @@ namespace Bloxstrap
_isInstalling = true;
- SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox...");
+ SetStatus(FreshInstall ? "Installing {product}..." : "Upgrading {product}...");
Directory.CreateDirectory(Paths.Base);
Directory.CreateDirectory(Paths.Downloads);
@@ -793,10 +878,12 @@ namespace Bloxstrap
{
Dialog.CancelEnabled = true;
Dialog.ProgressStyle = ProgressBarStyle.Continuous;
- }
- // compute total bytes to download
- _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize);
+ Dialog.ProgressMaximum = ProgressBarMaximum;
+
+ // compute total bytes to download
+ _progressIncrement = (double)ProgressBarMaximum / _versionPackageManifest.Sum(package => package.PackedSize);
+ }
foreach (Package package in _versionPackageManifest)
{
@@ -812,7 +899,7 @@ namespace Bloxstrap
// extract the package immediately after download asynchronously
// discard is just used to suppress the warning
- _ = ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}");
+ _ = Task.Run(() => ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}"));
}
if (_cancelFired)
@@ -824,7 +911,7 @@ namespace Bloxstrap
if (Dialog is not null)
{
Dialog.ProgressStyle = ProgressBarStyle.Marquee;
- SetStatus("Configuring Roblox...");
+ SetStatus("Configuring {product}...");
}
// wait for all packages to finish extracting, with an exception for the webview2 runtime installer
@@ -861,12 +948,12 @@ namespace Bloxstrap
}
}
- string oldVersionFolder = Path.Combine(Paths.Versions, App.State.Prop.VersionGuid);
+ string oldVersionFolder = Path.Combine(Paths.Versions, _versionGuid);
// move old compatibility flags for the old location
using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"))
{
- string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe");
+ string oldGameClientLocation = Path.Combine(oldVersionFolder, _playerFileName);
string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation);
if (appFlags is not null)
@@ -876,34 +963,34 @@ namespace Bloxstrap
appFlagsKey.DeleteValue(oldGameClientLocation);
}
}
+ }
- // 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 (!Process.GetProcessesByName(App.RobloxAppName).Any())
+ _versionGuid = _latestVersionGuid;
+
+ // 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 (!Process.GetProcessesByName(App.RobloxPlayerAppName).Any() && !Process.GetProcessesByName(App.RobloxStudioAppName).Any())
+ {
+ foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories())
{
- 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
{
- if (dir.Name == _latestVersionGuid || !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);
- }
+ dir.Delete(true);
+ }
+ catch (Exception ex)
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!");
+ App.Logger.WriteException(LOG_IDENT, ex);
}
}
}
- App.State.Prop.VersionGuid = _latestVersionGuid;
-
// don't register program size until the program is registered, which will be done after this
if (!App.IsFirstRun && !FreshInstall)
RegisterProgramSize();
@@ -990,7 +1077,7 @@ namespace Bloxstrap
{
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
- if (Process.GetProcessesByName("RobloxPlayerBeta").Any())
+ if (Process.GetProcessesByName(_playerFileName[..^4]).Any())
{
App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check");
return;
@@ -1182,6 +1269,7 @@ namespace Bloxstrap
Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
+ Filesystem.AssertReadOnly(fileVersionFolder);
File.Copy(fileModFolder, fileVersionFolder, true);
Filesystem.AssertReadOnly(fileVersionFolder);
@@ -1196,7 +1284,7 @@ namespace Bloxstrap
if (modFolderFiles.Contains(fileLocation))
continue;
- var package = PackageDirectories.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value));
+ var package = _packageDirectories.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value));
// package doesn't exist, likely mistakenly placed file
if (String.IsNullOrEmpty(package.Key))
@@ -1361,9 +1449,11 @@ namespace Bloxstrap
_totalDownloadedBytes += bytesRead;
UpdateProgressBar();
}
-
- if (MD5Hash.FromStream(fileStream) != package.Signature)
- throw new Exception("Signature does not match!");
+
+ string hash = MD5Hash.FromStream(fileStream);
+
+ if (hash != package.Signature)
+ throw new ChecksumFailedException($"Failed to verify download of {packageUrl}\n\nGot signature: {hash}\n\nPackage has been downloaded to {packageLocation}\n\nPlease send the file shown above in a bug report.");
App.Logger.WriteLine(LOG_IDENT, $"Finished downloading! ({totalBytesRead} bytes total)");
break;
@@ -1373,12 +1463,12 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})");
App.Logger.WriteException(LOG_IDENT, ex);
+ if (i >= maxTries || ex.GetType() == typeof(ChecksumFailedException))
+ throw;
+
if (File.Exists(packageLocation))
File.Delete(packageLocation);
- if (i >= maxTries)
- throw;
-
_totalDownloadedBytes -= totalBytesRead;
UpdateProgressBar();
@@ -1394,75 +1484,26 @@ namespace Bloxstrap
}
}
- private async Task ExtractPackage(Package package)
+ private Task ExtractPackage(Package package)
{
const string LOG_IDENT = "Bootstrapper::ExtractPackage";
if (_cancelFired)
- return;
+ return Task.CompletedTask;
string packageLocation = Path.Combine(Paths.Downloads, package.Signature);
- string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]);
+ string packageFolder = Path.Combine(_versionFolder, _packageDirectories[package.Name]);
- App.Logger.WriteLine(LOG_IDENT, $"Reading {package.Name}...");
+ App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}...");
- var archive = await Task.Run(() => ZipFile.OpenRead(packageLocation));
-
- App.Logger.WriteLine(LOG_IDENT, $"Read {package.Name}. Extracting to {packageFolder}...");
-
- // yeah so because roblox is roblox, these packages aren't actually valid zip files
- // besides the fact that they use backslashes instead of forward slashes for directories,
- // empty folders that *BEGIN* with a backslash in their fullname, but have an empty name are listed here for some reason...
-
- foreach (var entry in archive.Entries)
- {
- if (_cancelFired)
- return;
-
- if (String.IsNullOrEmpty(entry.Name))
- continue;
-
- string extractPath = Path.Combine(packageFolder, entry.FullName);
- string? directory = Path.GetDirectoryName(extractPath);
-
- if (directory is not null)
- Directory.CreateDirectory(directory);
-
- var fileManifest = _versionFileManifest.FirstOrDefault(x => x.Name == Path.Combine(PackageDirectories[package.Name], entry.FullName));
- string? signature = fileManifest?.Signature;
-
- if (File.Exists(extractPath))
- {
- if (signature is not null && MD5Hash.FromFile(extractPath) == signature)
- continue;
-
- File.Delete(extractPath);
- }
-
- bool retry = false;
-
- do
- {
- using var entryStream = entry.Open();
- using var fileStream = new FileStream(extractPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 0x1000);
- await entryStream.CopyToAsync(fileStream);
-
- if (signature is not null && MD5Hash.FromStream(fileStream) != signature)
- {
- if (retry)
- throw new AssertionException($"Checksum of {entry.FullName} post-extraction did not match manifest");
-
- retry = true;
- }
- }
- while (retry);
-
- File.SetLastWriteTime(extractPath, entry.LastWriteTime.DateTime);
- }
+ var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip();
+ fastZip.ExtractZip(packageLocation, packageFolder, null);
App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}");
_packagesExtracted += 1;
+
+ return Task.CompletedTask;
}
private async Task ExtractFileFromPackage(string packageName, string fileName)
@@ -1481,7 +1522,7 @@ namespace Bloxstrap
if (entry is null)
return;
- string extractionPath = Path.Combine(_versionFolder, PackageDirectories[package.Name], entry.FullName);
+ string extractionPath = Path.Combine(_versionFolder, _packageDirectories[package.Name], entry.FullName);
entry.ExtractToFile(extractionPath, true);
}
#endregion
diff --git a/Bloxstrap/Enums/LaunchMode.cs b/Bloxstrap/Enums/LaunchMode.cs
new file mode 100644
index 0000000..d1a34e9
--- /dev/null
+++ b/Bloxstrap/Enums/LaunchMode.cs
@@ -0,0 +1,9 @@
+namespace Bloxstrap.Enums
+{
+ public enum LaunchMode
+ {
+ Player,
+ Studio,
+ StudioAuth
+ }
+}
diff --git a/Bloxstrap/Exceptions/ChecksumFailedException.cs b/Bloxstrap/Exceptions/ChecksumFailedException.cs
new file mode 100644
index 0000000..95d8af2
--- /dev/null
+++ b/Bloxstrap/Exceptions/ChecksumFailedException.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Bloxstrap.Exceptions
+{
+ internal class ChecksumFailedException : Exception
+ {
+ public ChecksumFailedException(string message) : base(message)
+ {
+ }
+ }
+}
diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs
index eb66599..b574732 100644
--- a/Bloxstrap/FastFlagManager.cs
+++ b/Bloxstrap/FastFlagManager.cs
@@ -11,6 +11,7 @@ namespace Bloxstrap
// this is the value of the 'FStringPartTexturePackTablePre2022' flag
public const string OldTexturesFlagValue = "{\"foil\":{\"ids\":[\"rbxassetid://7546645012\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"brick\":{\"ids\":[\"rbxassetid://7546650097\",\"rbxassetid://7546645118\"],\"color\":[204,201,200,232]},\"cobblestone\":{\"ids\":[\"rbxassetid://7546652947\",\"rbxassetid://7546645118\"],\"color\":[212,200,187,250]},\"concrete\":{\"ids\":[\"rbxassetid://7546653951\",\"rbxassetid://7546654144\"],\"color\":[208,208,208,255]},\"diamondplate\":{\"ids\":[\"rbxassetid://7547162198\",\"rbxassetid://7546645118\"],\"color\":[170,170,170,255]},\"fabric\":{\"ids\":[\"rbxassetid://7547101130\",\"rbxassetid://7546645118\"],\"color\":[105,104,102,244]},\"glass\":{\"ids\":[\"rbxassetid://7547304948\",\"rbxassetid://7546645118\"],\"color\":[254,254,254,7]},\"granite\":{\"ids\":[\"rbxassetid://7547164710\",\"rbxassetid://7546645118\"],\"color\":[113,113,113,255]},\"grass\":{\"ids\":[\"rbxassetid://7547169285\",\"rbxassetid://7546645118\"],\"color\":[165,165,159,255]},\"ice\":{\"ids\":[\"rbxassetid://7547171356\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"marble\":{\"ids\":[\"rbxassetid://7547177270\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"metal\":{\"ids\":[\"rbxassetid://7547288171\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"pebble\":{\"ids\":[\"rbxassetid://7547291361\",\"rbxassetid://7546645118\"],\"color\":[208,208,208,255]},\"corrodedmetal\":{\"ids\":[\"rbxassetid://7547184629\",\"rbxassetid://7546645118\"],\"color\":[159,119,95,200]},\"sand\":{\"ids\":[\"rbxassetid://7547295153\",\"rbxassetid://7546645118\"],\"color\":[220,220,220,255]},\"slate\":{\"ids\":[\"rbxassetid://7547298114\",\"rbxassetid://7547298323\"],\"color\":[193,193,193,255]},\"wood\":{\"ids\":[\"rbxassetid://7547303225\",\"rbxassetid://7547298786\"],\"color\":[227,227,227,255]},\"woodplanks\":{\"ids\":[\"rbxassetid://7547332968\",\"rbxassetid://7546645118\"],\"color\":[212,209,203,255]},\"asphalt\":{\"ids\":[\"rbxassetid://9873267379\",\"rbxassetid://9438410548\"],\"color\":[123,123,123,234]},\"basalt\":{\"ids\":[\"rbxassetid://9873270487\",\"rbxassetid://9438413638\"],\"color\":[154,154,153,238]},\"crackedlava\":{\"ids\":[\"rbxassetid://9438582231\",\"rbxassetid://9438453972\"],\"color\":[74,78,80,156]},\"glacier\":{\"ids\":[\"rbxassetid://9438851661\",\"rbxassetid://9438453972\"],\"color\":[226,229,229,243]},\"ground\":{\"ids\":[\"rbxassetid://9439044431\",\"rbxassetid://9438453972\"],\"color\":[114,114,112,240]},\"leafygrass\":{\"ids\":[\"rbxassetid://9873288083\",\"rbxassetid://9438453972\"],\"color\":[121,117,113,234]},\"limestone\":{\"ids\":[\"rbxassetid://9873289812\",\"rbxassetid://9438453972\"],\"color\":[235,234,230,250]},\"mud\":{\"ids\":[\"rbxassetid://9873319819\",\"rbxassetid://9438453972\"],\"color\":[130,130,130,252]},\"pavement\":{\"ids\":[\"rbxassetid://9873322398\",\"rbxassetid://9438453972\"],\"color\":[142,142,144,236]},\"rock\":{\"ids\":[\"rbxassetid://9873515198\",\"rbxassetid://9438453972\"],\"color\":[154,154,154,248]},\"salt\":{\"ids\":[\"rbxassetid://9439566986\",\"rbxassetid://9438453972\"],\"color\":[220,220,221,255]},\"sandstone\":{\"ids\":[\"rbxassetid://9873521380\",\"rbxassetid://9438453972\"],\"color\":[174,171,169,246]},\"snow\":{\"ids\":[\"rbxassetid://9439632387\",\"rbxassetid://9438453972\"],\"color\":[218,218,218,255]}}";
+ public const string NewTexturesFlagValue = "{\"foil\":{\"ids\":[\"rbxassetid://9873266399\",\"rbxassetid://9438410239\"],\"color\":[238,238,238,255]},\"asphalt\":{\"ids\":[\"rbxassetid://9930003180\",\"rbxassetid://9438410548\"],\"color\":[227,227,228,234]},\"basalt\":{\"ids\":[\"rbxassetid://9920482224\",\"rbxassetid://9438413638\"],\"color\":[160,160,158,238]},\"brick\":{\"ids\":[\"rbxassetid://9920482992\",\"rbxassetid://9438453972\"],\"color\":[229,214,205,227]},\"cobblestone\":{\"ids\":[\"rbxassetid://9919719550\",\"rbxassetid://9438453972\"],\"color\":[218,219,219,243]},\"concrete\":{\"ids\":[\"rbxassetid://9920484334\",\"rbxassetid://9438453972\"],\"color\":[225,225,224,255]},\"crackedlava\":{\"ids\":[\"rbxassetid://9920485426\",\"rbxassetid://9438453972\"],\"color\":[76,79,81,156]},\"diamondplate\":{\"ids\":[\"rbxassetid://10237721036\",\"rbxassetid://9438453972\"],\"color\":[210,210,210,255]},\"fabric\":{\"ids\":[\"rbxassetid://9920517963\",\"rbxassetid://9438453972\"],\"color\":[221,221,221,255]},\"glacier\":{\"ids\":[\"rbxassetid://9920518995\",\"rbxassetid://9438453972\"],\"color\":[225,229,229,243]},\"glass\":{\"ids\":[\"rbxassetid://9873284556\",\"rbxassetid://9438453972\"],\"color\":[254,254,254,7]},\"granite\":{\"ids\":[\"rbxassetid://9920550720\",\"rbxassetid://9438453972\"],\"color\":[210,206,200,255]},\"grass\":{\"ids\":[\"rbxassetid://9920552044\",\"rbxassetid://9438453972\"],\"color\":[196,196,189,241]},\"ground\":{\"ids\":[\"rbxassetid://9920554695\",\"rbxassetid://9438453972\"],\"color\":[165,165,160,240]},\"ice\":{\"ids\":[\"rbxassetid://9920556429\",\"rbxassetid://9438453972\"],\"color\":[235,239,241,248]},\"leafygrass\":{\"ids\":[\"rbxassetid://9920558145\",\"rbxassetid://9438453972\"],\"color\":[182,178,175,234]},\"limestone\":{\"ids\":[\"rbxassetid://9920561624\",\"rbxassetid://9438453972\"],\"color\":[250,248,243,250]},\"marble\":{\"ids\":[\"rbxassetid://9873292869\",\"rbxassetid://9438453972\"],\"color\":[181,183,193,249]},\"metal\":{\"ids\":[\"rbxassetid://9920574966\",\"rbxassetid://9438453972\"],\"color\":[226,226,226,255]},\"mud\":{\"ids\":[\"rbxassetid://9920578676\",\"rbxassetid://9438453972\"],\"color\":[193,192,193,252]},\"pavement\":{\"ids\":[\"rbxassetid://9920580094\",\"rbxassetid://9438453972\"],\"color\":[218,218,219,236]},\"pebble\":{\"ids\":[\"rbxassetid://9920581197\",\"rbxassetid://9438453972\"],\"color\":[204,203,201,234]},\"plastic\":{\"ids\":[\"\",\"rbxassetid://9475422736\"],\"color\":[255,255,255,255]},\"rock\":{\"ids\":[\"rbxassetid://10129366149\",\"rbxassetid://9438453972\"],\"color\":[211,211,210,248]},\"corrodedmetal\":{\"ids\":[\"rbxassetid://9920589512\",\"rbxassetid://9439557520\"],\"color\":[206,177,163,180]},\"salt\":{\"ids\":[\"rbxassetid://9920590478\",\"rbxassetid://9438453972\"],\"color\":[249,249,249,255]},\"sand\":{\"ids\":[\"rbxassetid://9920591862\",\"rbxassetid://9438453972\"],\"color\":[218,216,210,240]},\"sandstone\":{\"ids\":[\"rbxassetid://9920596353\",\"rbxassetid://9438453972\"],\"color\":[241,234,230,246]},\"slate\":{\"ids\":[\"rbxassetid://9920600052\",\"rbxassetid://9439613006\"],\"color\":[235,234,235,254]},\"snow\":{\"ids\":[\"rbxassetid://9920620451\",\"rbxassetid://9438453972\"],\"color\":[239,240,240,255]},\"wood\":{\"ids\":[\"rbxassetid://9920625499\",\"rbxassetid://9439649548\"],\"color\":[217,209,208,255]},\"woodplanks\":{\"ids\":[\"rbxassetid://9920626896\",\"rbxassetid://9438453972\"],\"color\":[207,208,206,254]}}";
public static IReadOnlyDictionary PresetFlags = new Dictionary
{
@@ -25,9 +26,11 @@ namespace Bloxstrap
{ "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" },
{ "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" },
- { "Rendering.TexturePack", "FStringPartTexturePackTable2022" },
{ "Rendering.DisableScaling", "DFFlagDisableDPIScale" },
+ { "Rendering.Materials.NewTexturePack", "FStringPartTexturePackTable2022" },
+ { "Rendering.Materials.OldTexturePack", "FStringPartTexturePackTablePre2022" },
+
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
{ "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" },
@@ -80,6 +83,13 @@ namespace Bloxstrap
{ "8x MSAA", "8" }
};
+ public static IReadOnlyDictionary MaterialVersions => new Dictionary
+ {
+ { "Chosen by game", "None" },
+ { "Old (Pre-2022)", "NewTexturePack" },
+ { "New (2022)", "OldTexturePack" }
+ };
+
// this is one hell of a dictionary definition lmao
// since these all set the same flags, wouldn't making this use bitwise operators be better?
public static IReadOnlyDictionary> IGMenuVersions => new Dictionary>
diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs
index 6f1d650..5c22be6 100644
--- a/Bloxstrap/Models/State.cs
+++ b/Bloxstrap/Models/State.cs
@@ -3,7 +3,15 @@
public class State
{
public string LastEnrolledChannel { get; set; } = "";
- public string VersionGuid { get; set; } = "";
+
+ [Obsolete("Use PlayerVersionGuid instead", true)]
+ public string VersionGuid { set { PlayerVersionGuid = value; } }
+ public string PlayerVersionGuid { get; set; } = "";
+ public string StudioVersionGuid { get; set; } = "";
+
+ public int PlayerSize { get; set; } = 0;
+ public int StudioSize { get; set; } = 0;
+
public List ModManifest { get; set; } = new();
}
}
diff --git a/Bloxstrap/PackageMap.cs b/Bloxstrap/PackageMap.cs
new file mode 100644
index 0000000..6932921
--- /dev/null
+++ b/Bloxstrap/PackageMap.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Bloxstrap
+{
+ internal class PackageMap
+ {
+ public static IReadOnlyDictionary Player
+ {
+ get { return CombineDictionaries(_common, _playerOnly); }
+ }
+
+ public static IReadOnlyDictionary Studio
+ {
+ get { return CombineDictionaries(_common, _studioOnly); }
+ }
+
+ // in case a new package is added, you can find the corresponding directory
+ // by opening the stock bootstrapper in a hex editor
+ // TODO - there ideally should be a less static way to do this that's not hardcoded?
+ private static IReadOnlyDictionary _common = new Dictionary()
+ {
+ { "Libraries.zip", @"" },
+ { "shaders.zip", @"shaders\" },
+ { "ssl.zip", @"ssl\" },
+
+ // the runtime installer is only extracted if it needs installing
+ { "WebView2.zip", @"" },
+ { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" },
+
+ { "content-avatar.zip", @"content\avatar\" },
+ { "content-configs.zip", @"content\configs\" },
+ { "content-fonts.zip", @"content\fonts\" },
+ { "content-sky.zip", @"content\sky\" },
+ { "content-sounds.zip", @"content\sounds\" },
+ { "content-textures2.zip", @"content\textures\" },
+ { "content-models.zip", @"content\models\" },
+
+ { "content-textures3.zip", @"PlatformContent\pc\textures\" },
+ { "content-terrain.zip", @"PlatformContent\pc\terrain\" },
+ { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" },
+
+ { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" },
+ { "extracontent-translations.zip", @"ExtraContent\translations\" },
+ { "extracontent-models.zip", @"ExtraContent\models\" },
+ { "extracontent-textures.zip", @"ExtraContent\textures\" },
+ { "extracontent-places.zip", @"ExtraContent\places\" },
+ };
+
+ private static IReadOnlyDictionary _playerOnly = new Dictionary()
+ {
+ { "RobloxApp.zip", @"" }
+ };
+
+ private static IReadOnlyDictionary _studioOnly = new Dictionary()
+ {
+ { "RobloxStudio.zip", @"" },
+ { "ApplicationConfig.zip", @"ApplicationConfig\" },
+ { "content-studio_svg_textures.zip", @"content\studio_svg_textures\"},
+ { "content-qt_translations.zip", @"content\qt_translations\" },
+ { "content-api-docs.zip", @"content\api_docs\" },
+ { "extracontent-scripts.zip", @"ExtraContent\scripts\" },
+ { "BuiltInPlugins.zip", @"BuiltInPlugins\" },
+ { "BuiltInStandalonePlugins.zip", @"BuiltInStandalonePlugins\" },
+ { "LibrariesQt5.zip", @"" },
+ { "Plugins.zip", @"Plugins\" },
+ { "Qml.zip", @"Qml\" },
+ { "StudioFonts.zip", @"StudioFonts\" },
+ { "redist.zip", @"" },
+ };
+
+ private static Dictionary CombineDictionaries(IReadOnlyDictionary d1, IReadOnlyDictionary d2)
+ {
+ Dictionary newD = new Dictionary();
+
+ foreach (var d in d1)
+ newD[d.Key] = d.Value;
+
+ foreach (var d in d2)
+ newD[d.Key] = d.Value;
+
+ return newD;
+ }
+ }
+}
diff --git a/Bloxstrap/Properties/launchSettings.json b/Bloxstrap/Properties/launchSettings.json
index ee3606b..2cf74a5 100644
--- a/Bloxstrap/Properties/launchSettings.json
+++ b/Bloxstrap/Properties/launchSettings.json
@@ -22,6 +22,10 @@
"Bloxstrap (Deeplink)": {
"commandName": "Project",
"commandLineArgs": "roblox://experiences/start?placeId=95206881"
+ },
+ "Bloxstrap (Studio Launch)": {
+ "commandName": "Project",
+ "commandLineArgs": "-ide"
}
}
}
\ No newline at end of file
diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/ProtocolHandler.cs
index 44ff873..3193d63 100644
--- a/Bloxstrap/ProtocolHandler.cs
+++ b/Bloxstrap/ProtocolHandler.cs
@@ -7,6 +7,8 @@ namespace Bloxstrap
{
static class ProtocolHandler
{
+ private const string RobloxPlaceKey = "Roblox.Place";
+
// map uri keys to command line args
private static readonly IReadOnlyDictionary UriKeyArgMap = new Dictionary()
{
@@ -18,7 +20,12 @@ namespace Bloxstrap
{ "browsertrackerid", "-b " },
{ "robloxLocale", "--rloc " },
{ "gameLocale", "--gloc " },
- { "channel", "-channel " }
+ { "channel", "-channel " },
+ // studio
+ { "task", "-task " },
+ { "placeId", "-placeId " },
+ { "universeId", "-universeId " },
+ { "userId", "-userId " }
};
public static string ParseUri(string protocol)
@@ -108,9 +115,10 @@ namespace Bloxstrap
public static void Register(string key, string name, string handler)
{
string handlerArgs = $"\"{handler}\" %1";
- RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
- RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon");
- RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command");
+
+ using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
+ using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon");
+ using RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command");
if (uriKey.GetValue("") is null)
{
@@ -118,15 +126,44 @@ namespace Bloxstrap
uriKey.SetValue("URL Protocol", "");
}
- if ((string?)uriCommandKey.GetValue("") != handlerArgs)
+ if (uriCommandKey.GetValue("") as string != handlerArgs)
{
uriIconKey.SetValue("", handler);
uriCommandKey.SetValue("", handlerArgs);
}
+ }
- uriKey.Close();
- uriIconKey.Close();
- uriCommandKey.Close();
+ public static void RegisterRobloxPlace(string handler)
+ {
+ const string keyValue = "Roblox Place";
+ string handlerArgs = $"\"{handler}\" -ide \"%1\"";
+ string iconValue = $"{handler},0";
+
+ using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey(@"Software\Classes\" + RobloxPlaceKey);
+ using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon");
+ using RegistryKey uriOpenKey = uriKey.CreateSubKey(@"shell\Open");
+ using RegistryKey uriCommandKey = uriOpenKey.CreateSubKey(@"command");
+
+ if (uriKey.GetValue("") as string != keyValue)
+ uriKey.SetValue("", keyValue);
+
+ if (uriCommandKey.GetValue("") as string != handlerArgs)
+ uriCommandKey.SetValue("", handlerArgs);
+
+ if (uriOpenKey.GetValue("") as string != "Open")
+ uriOpenKey.SetValue("", "Open");
+
+ if (uriIconKey.GetValue("") as string != iconValue)
+ uriIconKey.SetValue("", iconValue);
+ }
+
+ public static void RegisterExtension(string key)
+ {
+ using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
+ uriKey.CreateSubKey(RobloxPlaceKey + @"\ShellNew");
+
+ if (uriKey.GetValue("") as string != RobloxPlaceKey)
+ uriKey.SetValue("", RobloxPlaceKey);
}
public static void Unregister(string key)
diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs
index 5ce4b19..2a3a606 100644
--- a/Bloxstrap/RobloxDeployment.cs
+++ b/Bloxstrap/RobloxDeployment.cs
@@ -74,22 +74,23 @@
return location;
}
- public static async Task GetInfo(string channel, bool extraInformation = false)
+ public static async Task GetInfo(string channel, bool extraInformation = false, string binaryType = "WindowsPlayer")
{
const string LOG_IDENT = "RobloxDeployment::GetInfo";
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel} (extraInformation={extraInformation})");
+ string cacheKey = $"{channel}-{binaryType}";
ClientVersion clientVersion;
- if (ClientVersionCache.ContainsKey(channel))
+ if (ClientVersionCache.ContainsKey(cacheKey))
{
App.Logger.WriteLine(LOG_IDENT, "Deploy information is cached");
- clientVersion = ClientVersionCache[channel];
+ clientVersion = ClientVersionCache[cacheKey];
}
else
{
- string path = $"/v2/client-version/WindowsPlayer/channel/{channel}";
+ string path = $"/v2/client-version/{binaryType}/channel/{channel}";
HttpResponseMessage deployInfoResponse;
try
@@ -152,7 +153,7 @@
}
}
- ClientVersionCache[channel] = clientVersion;
+ ClientVersionCache[cacheKey] = clientVersion;
return clientVersion;
}
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs
index 69d4b36..d59bdc5 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs
@@ -14,6 +14,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base
protected virtual string _message { get; set; } = "Please wait...";
protected virtual ProgressBarStyle _progressStyle { get; set; }
protected virtual int _progressValue { get; set; }
+ protected virtual int _progressMaximum { get; set; }
protected virtual bool _cancelEnabled { get; set; }
public string Message
@@ -40,6 +41,18 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base
}
}
+ public int ProgressMaximum
+ {
+ get => _progressMaximum;
+ set
+ {
+ if (InvokeRequired)
+ Invoke(() => _progressMaximum = value);
+ else
+ _progressMaximum = value;
+ }
+ }
+
public int ProgressValue
{
get => _progressValue;
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml
index 183be50..1a0b6c8 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml
+++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml
@@ -38,7 +38,7 @@
-
+
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs
index b01d083..4d709ec 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs
@@ -41,6 +41,16 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
}
}
+ public int ProgressMaximum
+ {
+ get => _viewModel.ProgressMaximum;
+ set
+ {
+ _viewModel.ProgressMaximum = value;
+ _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressMaximum));
+ }
+ }
+
public int ProgressValue
{
get => _viewModel.ProgressValue;
@@ -69,7 +79,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
public ByfronDialog()
{
- _viewModel = new ByfronDialogViewModel(this);
+ string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false);
+ _viewModel = new ByfronDialogViewModel(this, version);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml
index 40cc7f9..ce265a6 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml
+++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml
@@ -36,7 +36,7 @@
-
+
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs
index 7ab6cdc..780e75d 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs
@@ -42,6 +42,16 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
}
}
+ public int ProgressMaximum
+ {
+ get => _viewModel.ProgressMaximum;
+ set
+ {
+ _viewModel.ProgressMaximum = value;
+ _viewModel.OnPropertyChanged(nameof(_viewModel.ProgressMaximum));
+ }
+ }
+
public int ProgressValue
{
get => _viewModel.ProgressValue;
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs
index ce2a1e8..c1dd219 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs
@@ -21,6 +21,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
set => ProgressBar.Style = value;
}
+ protected override int _progressMaximum
+ {
+ get => ProgressBar.Maximum;
+ set => ProgressBar.Maximum = value;
+ }
+
protected override int _progressValue
{
get => ProgressBar.Value;
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs
index 5f92d4e..7248b1c 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs
@@ -20,6 +20,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
set => ProgressBar.Style = value;
}
+ protected override int _progressMaximum
+ {
+ get => ProgressBar.Maximum;
+ set => ProgressBar.Maximum = value;
+ }
+
protected override int _progressValue
{
get => ProgressBar.Value;
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs b/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs
index 4f9bb01..b00e8c6 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs
@@ -21,6 +21,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
set => ProgressBar.Style = value;
}
+ protected override int _progressMaximum
+ {
+ get => ProgressBar.Maximum;
+ set => ProgressBar.Maximum = value;
+ }
+
protected override int _progressValue
{
get => ProgressBar.Value;
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs b/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs
index 8a47f9c..c228bf8 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs
@@ -37,6 +37,18 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
}
}
+ protected sealed override int _progressMaximum
+ {
+ get => _dialogPage.ProgressBar?.Maximum ?? 0;
+ set
+ {
+ if (_dialogPage.ProgressBar is null)
+ return;
+
+ _dialogPage.ProgressBar.Maximum = value;
+ }
+ }
+
protected sealed override int _progressValue
{
get => _dialogPage.ProgressBar?.Value ?? 0;
diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml
index 58d48b3..95bb446 100644
--- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml
+++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml
@@ -60,6 +60,18 @@
+
diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs
index f2b8df0..4c4d192 100644
--- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs
+++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs
@@ -26,14 +26,16 @@ namespace Bloxstrap.UI.Elements.ContextMenu
private LogTracer? _logTracerWindow;
private ServerInformation? _serverInformationWindow;
+ private int? _processId;
- public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler)
+ public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler, int? processId)
{
InitializeComponent();
ApplyTheme();
_activityWatcher = activityWatcher;
_richPresenceHandler = richPresenceHandler;
+ _processId = processId;
if (_activityWatcher is not null)
{
@@ -47,6 +49,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu
if (_richPresenceHandler is not null)
RichPresenceMenuItem.Visibility = Visibility.Visible;
+ if (_processId is not null)
+ CloseRobloxMenuItem.Visibility = Visibility.Visible;
+
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
}
@@ -118,5 +123,21 @@ namespace Bloxstrap.UI.Elements.ContextMenu
_logTracerWindow.Activate();
}
+
+ private void CloseRobloxMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ MessageBoxResult result = Controls.ShowMessageBox(
+ "Are you sure you want to close Roblox? This will forcefully end the process.",
+ MessageBoxImage.Warning,
+ MessageBoxButton.YesNo
+ );
+
+ if (result != MessageBoxResult.Yes)
+ return;
+
+ using Process process = Process.GetProcessById((int)_processId!);
+ process.CloseMainWindow();
+ process.Close();
+ }
}
}
diff --git a/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml
index df04fad..ce75096 100644
--- a/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml
+++ b/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml
@@ -221,9 +221,15 @@
-
+
-
+
+
+
+
+
+
+
diff --git a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml
index 666e665..66bb6c4 100644
--- a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml
+++ b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml
@@ -150,17 +150,17 @@
-
+
-
+
-
+
diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml
index 51727ff..e9608ba 100644
--- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml
+++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml
@@ -121,33 +121,31 @@
-
-
-
-
-
-
-
- Forces every in-game font to be a font that you choose.
-
-
-
+
+
+
-
-
+
+
+ Forces every in-game font to be a font that you choose.
+
-
-
-
-
-
-
- A Windows feature that intends to improve fullscreen performance. See here for more information.
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ A Windows feature that intends to improve fullscreen performance. See here for more information.
+
+
+
+
+
diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs
index d46903e..8ae448b 100644
--- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs
+++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs
@@ -16,7 +16,7 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
// fullscreen optimizations were only added in windows 10 build 17093
if (Environment.OSVersion.Version.Build < 17093)
- this.MiscellaneousOptions.Visibility = Visibility.Collapsed;
+ this.FullscreenOptimizationsToggle.Visibility = Visibility.Collapsed;
}
}
}
diff --git a/Bloxstrap/UI/IBootstrapperDialog.cs b/Bloxstrap/UI/IBootstrapperDialog.cs
index 4b44900..e15f7fb 100644
--- a/Bloxstrap/UI/IBootstrapperDialog.cs
+++ b/Bloxstrap/UI/IBootstrapperDialog.cs
@@ -9,6 +9,7 @@ namespace Bloxstrap.UI
string Message { get; set; }
ProgressBarStyle ProgressStyle { get; set; }
int ProgressValue { get; set; }
+ int ProgressMaximum { get; set; }
bool CancelEnabled { get; set; }
void ShowBootstrapper();
diff --git a/Bloxstrap/UI/NotifyIconWrapper.cs b/Bloxstrap/UI/NotifyIconWrapper.cs
index 6eb8e06..8ed8fea 100644
--- a/Bloxstrap/UI/NotifyIconWrapper.cs
+++ b/Bloxstrap/UI/NotifyIconWrapper.cs
@@ -16,6 +16,7 @@ namespace Bloxstrap.UI
private ActivityWatcher? _activityWatcher;
private DiscordRichPresence? _richPresenceHandler;
+ private int? _processId;
EventHandler? _alertClickHandler;
@@ -52,6 +53,14 @@ namespace Bloxstrap.UI
if (App.Settings.Prop.ShowServerDetails)
_activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin);
}
+
+ public void SetProcessId(int processId)
+ {
+ if (_processId is not null)
+ return;
+
+ _processId = processId;
+ }
#endregion
#region Context menu
@@ -62,7 +71,7 @@ namespace Bloxstrap.UI
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
- _menuContainer = new(_activityWatcher, _richPresenceHandler);
+ _menuContainer = new(_activityWatcher, _richPresenceHandler, _processId);
_menuContainer.ShowDialog();
}
diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs
index 593921d..2a5c523 100644
--- a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs
@@ -16,6 +16,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
public ImageSource Icon { get; set; } = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
public string Message { get; set; } = "Please wait...";
public bool ProgressIndeterminate { get; set; } = true;
+ public int ProgressMaximum { get; set; } = 0;
public int ProgressValue { get; set; } = 0;
public bool CancelEnabled { get; set; } = false;
diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs
index 5f16722..f652499 100644
--- a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs
@@ -16,26 +16,11 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
public Visibility VersionTextVisibility => CancelEnabled ? Visibility.Collapsed : Visibility.Visible;
- public string VersionText
- {
- get
- {
- string playerLocation = Path.Combine(Paths.Versions, App.State.Prop.VersionGuid, "RobloxPlayerBeta.exe");
-
- if (!File.Exists(playerLocation))
- return "";
-
- FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(playerLocation);
-
- if (versionInfo.ProductVersion is null)
- return "";
-
- return versionInfo.ProductVersion.Replace(", ", ".");
- }
- }
-
- public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog)
+ public string VersionText { get; init; }
+
+ public ByfronDialogViewModel(IBootstrapperDialog dialog, string version) : base(dialog)
{
+ VersionText = version;
}
}
}
diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs
index 28c4bc7..de074b9 100644
--- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs
@@ -2,7 +2,8 @@
{
public class BehaviourViewModel : NotifyPropertyChangedViewModel
{
- private string _oldVersionGuid = "";
+ private string _oldPlayerVersionGuid = "";
+ private string _oldStudioVersionGuid = "";
public BehaviourViewModel()
{
@@ -108,17 +109,22 @@
public bool ForceRobloxReinstallation
{
- get => String.IsNullOrEmpty(App.State.Prop.VersionGuid);
+ // wouldnt it be better to check old version guids?
+ // what about fresh installs?
+ get => String.IsNullOrEmpty(App.State.Prop.PlayerVersionGuid) && String.IsNullOrEmpty(App.State.Prop.StudioVersionGuid);
set
{
if (value)
{
- _oldVersionGuid = App.State.Prop.VersionGuid;
- App.State.Prop.VersionGuid = "";
+ _oldPlayerVersionGuid = App.State.Prop.PlayerVersionGuid;
+ _oldStudioVersionGuid = App.State.Prop.StudioVersionGuid;
+ App.State.Prop.PlayerVersionGuid = "";
+ App.State.Prop.StudioVersionGuid = "";
}
else
{
- App.State.Prop.VersionGuid = _oldVersionGuid;
+ App.State.Prop.PlayerVersionGuid = _oldPlayerVersionGuid;
+ App.State.Prop.StudioVersionGuid = _oldStudioVersionGuid;
}
}
}
diff --git a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs
index 8dee2e5..8114c83 100644
--- a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs
@@ -82,10 +82,21 @@ namespace Bloxstrap.UI.ViewModels.Menu
set => App.FastFlags.SetPreset("UI.Menu.GraphicsSlider", value ? "True" : null);
}
- public bool Pre2022TexturesEnabled
+ public IReadOnlyDictionary MaterialVersions => FastFlagManager.MaterialVersions;
+
+ public string SelectedMaterialVersion
{
- get => App.FastFlags.GetPreset("Rendering.TexturePack") == FastFlagManager.OldTexturesFlagValue;
- set => App.FastFlags.SetPreset("Rendering.TexturePack", value ? FastFlagManager.OldTexturesFlagValue : null);
+ get
+ {
+ string oldMaterials = App.FastFlags.GetPresetEnum(MaterialVersions, "Rendering.Materials", FastFlagManager.OldTexturesFlagValue);
+
+ if (oldMaterials != "Chosen by game")
+ return oldMaterials;
+
+ return App.FastFlags.GetPresetEnum(MaterialVersions, "Rendering.Materials", FastFlagManager.NewTexturesFlagValue);
+ }
+
+ set => App.FastFlags.SetPresetEnum("Rendering.Materials", MaterialVersions[value], MaterialVersions[value] == "NewTexturePack" ? FastFlagManager.OldTexturesFlagValue : FastFlagManager.NewTexturesFlagValue);
}
public IReadOnlyDictionary> IGMenuVersions => FastFlagManager.IGMenuVersions;
diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs
index 565dc79..7f33a19 100644
--- a/Bloxstrap/Utilities.cs
+++ b/Bloxstrap/Utilities.cs
@@ -47,5 +47,23 @@ namespace Bloxstrap
return version1.CompareTo(version2);
}
+
+ public static string GetRobloxVersion(bool studio)
+ {
+ string versionGuid = studio ? App.State.Prop.StudioVersionGuid : App.State.Prop.PlayerVersionGuid;
+ string fileName = studio ? "RobloxStudioBeta.exe" : "RobloxPlayerBeta.exe";
+
+ string playerLocation = Path.Combine(Paths.Versions, versionGuid, fileName);
+
+ if (!File.Exists(playerLocation))
+ return "";
+
+ FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(playerLocation);
+
+ if (versionInfo.ProductVersion is null)
+ return "";
+
+ return versionInfo.ProductVersion.Replace(", ", ".");
+ }
}
}
diff --git a/Bloxstrap/Utility/Filesystem.cs b/Bloxstrap/Utility/Filesystem.cs
index b9064fe..ff904ac 100644
--- a/Bloxstrap/Utility/Filesystem.cs
+++ b/Bloxstrap/Utility/Filesystem.cs
@@ -24,7 +24,7 @@ namespace Bloxstrap.Utility
{
var fileInfo = new FileInfo(filePath);
- if (!fileInfo.IsReadOnly)
+ if (!fileInfo.Exists || !fileInfo.IsReadOnly)
return;
fileInfo.IsReadOnly = false;
diff --git a/wpfui b/wpfui
index 55d5ca0..2a50f38 160000
--- a/wpfui
+++ b/wpfui
@@ -1 +1 @@
-Subproject commit 55d5ca08f9a1d7623f9a7e386e1f4ac9f2d024a7
+Subproject commit 2a50f387e6c3b0a9160f3ce42bc95fbb7185e87d