diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 12b62fa95..fddcc1a2e 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 @@ public partial class App : Application { 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 @@ public partial class App : Application ) ); +#if RELEASE private static bool _showingExceptionDialog = false; +#endif public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) { @@ -120,6 +124,10 @@ protected override void OnStartup(StartupEventArgs e) 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,8 @@ protected override void OnStartup(StartupEventArgs e) #endif string commandLine = ""; + bool isStudioLaunch = false; + bool isStudioAuth = false; if (IsMenuLaunch) { @@ -227,6 +237,25 @@ protected override void OnStartup(StartupEventArgs e) commandLine = $"--app --deeplink {LaunchArgs[0]}"; } + else if (LaunchArgs[0].StartsWith("roblox-studio:")) + { + commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); + if (!commandLine.Contains("-startEvent")) + commandLine += " -startEvent www.roblox.com/robloxQTStudioStartedEvent"; + isStudioLaunch = true; + } + else if (LaunchArgs[0].StartsWith("roblox-studio-auth:")) + { + commandLine = HttpUtility.UrlDecode(LaunchArgs[0]); + isStudioLaunch = true; + isStudioAuth = true; + } + else if (LaunchArgs[0] == "-ide") + { + isStudioLaunch = true; + if (LaunchArgs.Length >= 2) + commandLine = $"-task EditFile -localPlaceFile \"{LaunchArgs[1]}\""; + } else { commandLine = "--app"; @@ -244,7 +273,7 @@ protected override void OnStartup(StartupEventArgs e) // 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, isStudioLaunch, isStudioAuth); IBootstrapperDialog? dialog = null; if (!IsQuiet) @@ -261,7 +290,7 @@ protected override void OnStartup(StartupEventArgs e) Mutex? singletonMutex = null; - if (Settings.Prop.MultiInstanceLaunching) + if (Settings.Prop.MultiInstanceLaunching && !isStudioLaunch) { Logger.WriteLine(LOG_IDENT, "Creating singleton mutex"); diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 8ce13881c..36a6796b3 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 45d5c88d3..dfeb1b8d7 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -11,39 +11,7 @@ public class Bootstrapper { #region Properties private const int ProgressBarMaximum = 10000; - - // 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 string AppSettings = "\r\n" + "\r\n" + @@ -53,15 +21,50 @@ public class Bootstrapper 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 => _studioLaunch ? "RobloxStudioBeta.exe" : "RobloxPlayerBeta.exe"; + // TODO: change name + private string _playerLocation => Path.Combine(_versionFolder, _playerFileName); private string _launchCommandLine; + private bool _studioLaunch; + private bool _studioAuth; + + private string _versionGuid + { + get + { + return _studioLaunch ? App.State.Prop.StudioVersionGuid : App.State.Prop.PlayerVersionGuid; + } + + set + { + if (_studioLaunch) + App.State.Prop.StudioVersionGuid = value; + else + App.State.Prop.PlayerVersionGuid = value; + } + } + + private int _distributionSize + { + get + { + return _studioLaunch ? App.State.Prop.StudioSize : App.State.Prop.PlayerSize; + } + + set + { + if (_studioLaunch) + App.State.Prop.StudioSize = value; + else + App.State.Prop.PlayerSize = value; + } + } private string _latestVersionGuid = null!; private PackageManifest _versionPackageManifest = null!; - private FileManifest _versionFileManifest = null!; private string _versionFolder = null!; private bool _isInstalling = false; @@ -70,19 +73,32 @@ public class Bootstrapper private int _packagesExtracted = 0; private bool _cancelFired = false; + private IReadOnlyDictionary _packageDirectories; + public IBootstrapperDialog? Dialog = null; + public bool IsStudioLaunch => _studioLaunch; #endregion #region Core - public Bootstrapper(string launchCommandLine) + public Bootstrapper(string launchCommandLine, bool studioLaunch, bool studioAuth) { _launchCommandLine = launchCommandLine; + _studioLaunch = studioLaunch; + _studioAuth = studioAuth; + + _packageDirectories = _studioLaunch ? PackageMap.Studio : PackageMap.Player; } private void SetStatus(string message) { App.Logger.WriteLine("Bootstrapper::SetStatus", message); + string productName = "Roblox"; + if (_studioLaunch) + productName += " Studio"; + + message = message.Replace("{product}", productName); + // yea idk if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) message = message.Replace("...", ""); @@ -186,7 +202,7 @@ public async Task Run() 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) @@ -226,18 +242,20 @@ private async Task CheckLatestVersion() ClientVersion clientVersion; + string binaryType = _studioLaunch ? "WindowsStudio64" : "WindowsPlayer"; + 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) @@ -260,21 +278,20 @@ private async Task CheckLatestVersion() 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) { @@ -297,14 +314,17 @@ private async Task StartRoblox() return; } - _launchCommandLine = _launchCommandLine.Replace("LAUNCHTIMEPLACEHOLDER", DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString()); + if (!_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; @@ -316,6 +336,13 @@ private async Task StartRoblox() WorkingDirectory = _versionFolder }; + if (_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(startInfo)!) @@ -329,7 +356,8 @@ private async Task StartRoblox() App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid})"); - using (SystemEvent startEvent = new("www.roblox.com/robloxStartedEvent")) + string eventName = _studioLaunch ? "www.roblox.com/robloxQTStudioStartedEvent" : "www.roblox.com/robloxStartedEvent"; + using (SystemEvent startEvent = new(eventName)) { bool startEventFired = await startEvent.WaitForEvent(); @@ -339,7 +367,8 @@ private async Task StartRoblox() return; } - App.NotifyIcon?.SetProcessId(gameClientPid); + if (App.Settings.Prop.EnableActivityTracking && !_studioLaunch) + App.NotifyIcon?.SetProcessId(gameClientPid); if (App.Settings.Prop.EnableActivityTracking) { @@ -488,7 +517,10 @@ public void RegisterProgramSize() 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); @@ -507,6 +539,12 @@ public static void CheckInstall() 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); + + ProtocolHandler.RegisterRobloxPlace(Paths.Application); + ProtocolHandler.RegisterExtension(".rbxl"); + ProtocolHandler.RegisterExtension(".rbxlx"); if (Environment.ProcessPath is not null && Environment.ProcessPath != Paths.Application) { @@ -540,6 +578,7 @@ public static void CheckInstall() 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) { @@ -648,7 +687,7 @@ private void Uninstall() 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"); @@ -663,7 +702,13 @@ private void Uninstall() 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(); @@ -680,16 +725,17 @@ private void Uninstall() 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 { @@ -701,6 +747,27 @@ private void Uninstall() 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 @@ -731,7 +798,7 @@ private void Uninstall() 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) @@ -785,7 +852,7 @@ private async Task InstallLatestVersion() _isInstalling = true; - SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); + SetStatus(FreshInstall ? "Installing {product}..." : "Upgrading {product}..."); Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Downloads); @@ -832,7 +899,7 @@ private async Task InstallLatestVersion() // 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) @@ -844,7 +911,7 @@ private async Task InstallLatestVersion() 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 @@ -881,12 +948,12 @@ private async Task InstallLatestVersion() } } - 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) @@ -896,34 +963,34 @@ private async Task InstallLatestVersion() 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 == _latestVersionGuid || !dir.Name.StartsWith("version-")) - continue; + 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}"); + 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); - } + try + { + 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(); @@ -1010,7 +1077,7 @@ private async Task ApplyModifications() { 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; @@ -1217,7 +1284,7 @@ private async Task ApplyModifications() 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)) @@ -1417,75 +1484,26 @@ private async Task DownloadPackage(Package package) } } - 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]); - - App.Logger.WriteLine(LOG_IDENT, $"Reading {package.Name}..."); - - var archive = await Task.Run(() => ZipFile.OpenRead(packageLocation)); - - App.Logger.WriteLine(LOG_IDENT, $"Read {package.Name}. Extracting to {packageFolder}..."); + string packageFolder = Path.Combine(_versionFolder, _packageDirectories[package.Name]); - // 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... + App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); - 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) @@ -1504,7 +1522,7 @@ private async Task ExtractFileFromPackage(string packageName, string fileName) 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/Models/State.cs b/Bloxstrap/Models/State.cs index 6f1d65041..5c22be6e4 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 000000000..6932921f2 --- /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 ee3606b71..2cf74a520 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 44ff873a9..3193d63ef 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 @@ static class ProtocolHandler { "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 @@ public static void EnrollChannel(string channel) 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 @@ public static void Register(string key, string name, string handler) uriKey.SetValue("URL Protocol", ""); } - if ((string?)uriCommandKey.GetValue("") != handlerArgs) + if (uriCommandKey.GetValue("") as string != handlerArgs) { uriIconKey.SetValue("", handler); uriCommandKey.SetValue("", handlerArgs); } + } + + 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"); - uriKey.Close(); - uriIconKey.Close(); - uriCommandKey.Close(); + 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 cdeda888a..3d6d13b1b 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -69,22 +69,23 @@ public static string GetLocation(string resource, string? channel = null) 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 @@ -147,7 +148,7 @@ public static async Task GetInfo(string channel, bool extraInform } } - ClientVersionCache[channel] = clientVersion; + ClientVersionCache[cacheKey] = clientVersion; return clientVersion; } diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs index 83dc8185b..4d709ec7e 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs @@ -79,7 +79,8 @@ public bool CancelEnabled 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/Menu/Pages/AboutPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml index df04fad28..ce75096a0 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/ViewModels/Bootstrapper/ByfronDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs index 5f1672273..f652499cc 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs @@ -16,26 +16,11 @@ public class ByfronDialogViewModel : BootstrapperDialogViewModel 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 string VersionText { get; init; } - public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) + 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 28c4bc786..de074b949 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 string SelectedChannelChangeMode 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/Utilities.cs b/Bloxstrap/Utilities.cs index 565dc7929..7f33a1913 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -47,5 +47,23 @@ public static int CompareVersions(string versionStr1, string versionStr2) 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(", ", "."); + } } }