From d15e910904468da0b7c30d4391c39dcafb68b277 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 19 Aug 2024 16:27:42 +0100 Subject: [PATCH] Abstract differences in handling player and studio --- Bloxstrap/AppData/CommonAppData.cs | 63 +++++++++++++++++++ Bloxstrap/AppData/IAppData.cs | 23 +++++++ Bloxstrap/AppData/RobloxPlayerData.cs | 26 ++++++++ Bloxstrap/AppData/RobloxStudioData.cs | 42 +++++++++++++ Bloxstrap/Bootstrapper.cs | 46 ++++++-------- Bloxstrap/PackageMap.cs | 88 --------------------------- 6 files changed, 174 insertions(+), 114 deletions(-) create mode 100644 Bloxstrap/AppData/CommonAppData.cs create mode 100644 Bloxstrap/AppData/IAppData.cs create mode 100644 Bloxstrap/AppData/RobloxPlayerData.cs create mode 100644 Bloxstrap/AppData/RobloxStudioData.cs delete mode 100644 Bloxstrap/PackageMap.cs diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs new file mode 100644 index 0000000..5b202ab --- /dev/null +++ b/Bloxstrap/AppData/CommonAppData.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.AppData +{ + public abstract class CommonAppData + { + // in case a new package is added, you can find the corresponding directory + // by opening the stock bootstrapper in a hex editor + private IReadOnlyDictionary _commonMap { get; } = new Dictionary() + { + { "Libraries.zip", @"" }, + { "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\" }, + }; + + public virtual IReadOnlyDictionary PackageDirectoryMap { get; set; } + + public CommonAppData() + { + if (PackageDirectoryMap is null) + { + PackageDirectoryMap = _commonMap; + return; + } + + var merged = new Dictionary(); + + foreach (var entry in _commonMap) + merged[entry.Key] = entry.Value; + + foreach (var entry in PackageDirectoryMap) + merged[entry.Key] = entry.Value; + + PackageDirectoryMap = merged; + } + } +} diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs new file mode 100644 index 0000000..c637027 --- /dev/null +++ b/Bloxstrap/AppData/IAppData.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.AppData +{ + internal interface IAppData + { + string ProductName { get; } + + string BinaryType { get; } + + string RegistryName { get; } + + string ExecutableName { get; } + + string StartEvent { get; } + + IReadOnlyDictionary PackageDirectoryMap { get; set; } + } +} diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs new file mode 100644 index 0000000..3bc8785 --- /dev/null +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.AppData +{ + public class RobloxPlayerData : CommonAppData, IAppData + { + public string ProductName { get; } = "Roblox"; + + public string BinaryType { get; } = "WindowsPlayer"; + + public string RegistryName { get; } = "RobloxPlayer"; + + public string ExecutableName { get; } = "RobloxPlayerBeta.exe"; + + public string StartEvent { get; } = "www.roblox.com/robloxStartedEvent"; + + public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() + { + { "RobloxApp.zip", @"" } + }; + } +} diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs new file mode 100644 index 0000000..2c40ef8 --- /dev/null +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.AppData +{ + public class RobloxStudioData : CommonAppData, IAppData + { + public string ProductName { get; } = "Roblox Studio"; + + public string BinaryType { get; } = "WindowsStudio64"; + + public string RegistryName { get; } = "RobloxStudio"; + + public string ExecutableName { get; } = "RobloxStudioBeta.exe"; + + public string StartEvent { get; } = "www.roblox.com/robloxStudioStartedEvent"; + + public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() + { + { "RobloxStudio.zip", @"" }, + { "redist.zip", @"" }, + { "LibrariesQt5.zip", @"" }, + + { "content-studio_svg_textures.zip", @"content\studio_svg_textures\"}, + { "content-qt_translations.zip", @"content\qt_translations\" }, + { "content-api-docs.zip", @"content\api_docs\" }, + + { "extracontent-scripts.zip", @"ExtraContent\scripts\" }, + + { "BuiltInPlugins.zip", @"BuiltInPlugins\" }, + { "BuiltInStandalonePlugins.zip", @"BuiltInStandalonePlugins\" }, + + { "ApplicationConfig.zip", @"ApplicationConfig\" }, + { "Plugins.zip", @"Plugins\" }, + { "Qml.zip", @"Qml\" }, + { "StudioFonts.zip", @"StudioFonts\" } + }; + } +} diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 280d577..c23d17d 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -5,6 +5,7 @@ using Microsoft.Win32; using Bloxstrap.Integrations; using Bloxstrap.Resources; +using Bloxstrap.AppData; namespace Bloxstrap { @@ -24,8 +25,9 @@ namespace Bloxstrap private bool FreshInstall => String.IsNullOrEmpty(_versionGuid); - private string _playerFileName => _launchMode == LaunchMode.Player ? "RobloxPlayerBeta.exe" : "RobloxStudioBeta.exe"; - private string _playerLocation => Path.Combine(_versionFolder, _playerFileName); + private IAppData AppData; + + private string _playerLocation => Path.Combine(_versionFolder, AppData.ExecutableName); private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs; private LaunchMode _launchMode = App.LaunchSettings.RobloxLaunchMode; @@ -73,8 +75,6 @@ namespace Bloxstrap private int _packagesExtracted = 0; private bool _cancelFired = false; - private IReadOnlyDictionary _packageDirectories; - public IBootstrapperDialog? Dialog = null; public bool IsStudioLaunch => _launchMode != LaunchMode.Player; @@ -85,19 +85,17 @@ namespace Bloxstrap { _installWebView2 = installWebView2; - _packageDirectories = _launchMode == LaunchMode.Player ? PackageMap.Player : PackageMap.Studio; + if (_launchMode == LaunchMode.Player) + AppData = new RobloxPlayerData(); + else + AppData = new RobloxStudioData(); } private void SetStatus(string message) { App.Logger.WriteLine("Bootstrapper::SetStatus", message); - string productName = "Roblox"; - - if (_launchMode != LaunchMode.Player) - productName = "Roblox Studio"; - - message = message.Replace("{product}", productName); + message = message.Replace("{product}", AppData.ProductName); if (Dialog is not null) Dialog.Message = message; @@ -229,9 +227,7 @@ namespace Bloxstrap string channel = "production"; - string keyPath = _launchMode == LaunchMode.Player ? "RobloxPlayer" : "RobloxStudio"; - - using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{keyPath}\\Channel"); + using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel"); var match = Regex.Match(App.LaunchSettings.RobloxLaunchArgs, "channel:([a-zA-Z0-9-_]+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); @@ -246,11 +242,9 @@ namespace Bloxstrap ClientVersion clientVersion; - string binaryType = _launchMode == LaunchMode.Player ? "WindowsPlayer" : "WindowsStudio64"; - try { - clientVersion = await RobloxDeployment.GetInfo(channel, binaryType); + clientVersion = await RobloxDeployment.GetInfo(channel, AppData.BinaryType); } catch (HttpResponseException ex) { @@ -263,7 +257,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {channel} to {RobloxDeployment.DefaultChannel} because HTTP {(int)ex.ResponseMessage.StatusCode}"); channel = RobloxDeployment.DefaultChannel; - clientVersion = await RobloxDeployment.GetInfo(channel, binaryType); + clientVersion = await RobloxDeployment.GetInfo(channel, AppData.BinaryType); } if (clientVersion.IsBehindDefaultChannel) @@ -271,7 +265,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {channel} to {RobloxDeployment.DefaultChannel} because channel is behind production"); channel = RobloxDeployment.DefaultChannel; - clientVersion = await RobloxDeployment.GetInfo(channel, binaryType); + clientVersion = await RobloxDeployment.GetInfo(channel, AppData.BinaryType); } key.SetValue("www.roblox.com", channel); @@ -325,13 +319,13 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid})"); - string eventName = _launchMode == LaunchMode.Player ? "www.roblox.com/robloxStartedEvent" : "www.roblox.com/robloxQTStudioStartedEvent"; - using (SystemEvent startEvent = new(eventName)) + using (var startEvent = new SystemEvent(AppData.StartEvent)) { bool startEventFired = await startEvent.WaitForEvent(); startEvent.Close(); + // TODO: this cannot silently exit like this if (!startEventFired) return; } @@ -695,7 +689,7 @@ namespace Bloxstrap // 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, _playerFileName); + string oldGameClientLocation = Path.Combine(oldVersionFolder, AppData.ExecutableName); string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); if (appFlags is not null) @@ -812,7 +806,7 @@ namespace Bloxstrap { const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - if (Process.GetProcessesByName(_playerFileName[..^4]).Any()) + if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) { App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); return; @@ -958,7 +952,7 @@ namespace Bloxstrap if (modFolderFiles.Contains(fileLocation)) continue; - var package = _packageDirectories.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); // package doesn't exist, likely mistakenly placed file if (String.IsNullOrEmpty(package.Key)) @@ -1128,7 +1122,7 @@ namespace Bloxstrap return Task.CompletedTask; string packageLocation = Path.Combine(Paths.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, _packageDirectories[package.Name]); + string packageFolder = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name]); App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); @@ -1158,7 +1152,7 @@ namespace Bloxstrap if (entry is null) return; - string extractionPath = Path.Combine(_versionFolder, _packageDirectories[package.Name], entry.FullName); + string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName); entry.ExtractToFile(extractionPath, true); } #endregion diff --git a/Bloxstrap/PackageMap.cs b/Bloxstrap/PackageMap.cs deleted file mode 100644 index 6932921..0000000 --- a/Bloxstrap/PackageMap.cs +++ /dev/null @@ -1,88 +0,0 @@ -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; - } - } -}