diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 81cecce..b675e85 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Security.Cryptography; using System.Windows; using System.Windows.Shell; @@ -232,6 +233,37 @@ namespace Bloxstrap } } + /// + /// This should only ever return true if we're launched from the Roblox installer path, created by the private channel launch + /// + private static bool IsRobloxInstallerPath(string path, [NotNullWhen(true)] out string? truePath) + { + truePath = null; + + DirectoryInfo info = new DirectoryInfo(path); + if (info.Name != "RobloxPlayerInstaller.exe" && info.Name != "RobloxStudioInstaller.exe") + return false; + + DirectoryInfo? versionDir = info.Parent; + if (versionDir == null || !versionDir.Name.StartsWith("version-", StringComparison.Ordinal)) + return false; + + DirectoryInfo? versionsDir = versionDir.Parent; + if (versionsDir == null || versionsDir.Name != "Versions") + return false; + + DirectoryInfo? bloxstrapDir = versionsDir.Parent; + if (bloxstrapDir == null) + return false; + + string bloxstrapPath = Path.Combine(bloxstrapDir.FullName, "Bloxstrap.exe"); + if (!File.Exists(bloxstrapPath)) + return false; + + truePath = bloxstrapPath; + return true; + } + protected override void OnStartup(StartupEventArgs e) { const string LOG_IDENT = "App::OnStartup"; @@ -277,6 +309,13 @@ namespace Bloxstrap HttpClient.Timeout = TimeSpan.FromSeconds(30); HttpClient.DefaultRequestHeaders.Add("User-Agent", userAgent); + // make sure we're in the correct place + if (IsRobloxInstallerPath(Paths.Process, out string? truePath)) + { + Process.Start(truePath, e.Args); + return; + } + LaunchSettings = new LaunchSettings(e.Args); // installation check begins here diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs index b19aa95..abba7ea 100644 --- a/Bloxstrap/AppData/IAppData.cs +++ b/Bloxstrap/AppData/IAppData.cs @@ -14,6 +14,8 @@ string ExecutablePath { get; } + string RobloxInstallerExecutableName { get; } + AppState State { get; } IReadOnlyDictionary PackageDirectoryMap { get; set; } diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index bba6cc8..1f854eb 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -18,6 +18,8 @@ namespace Bloxstrap.AppData public override AppState State => App.RobloxState.Prop.Player; + public string RobloxInstallerExecutableName => "RobloxPlayerInstaller.exe"; + public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { { "RobloxApp.zip", @"" } diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 18c8e36..a490173 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -12,6 +12,8 @@ public override AppState State => App.RobloxState.Prop.Studio; + public string RobloxInstallerExecutableName => "RobloxStudioInstaller.exe"; + public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() { { "RobloxStudio.zip", @"" }, diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 1531fbe..b30df07 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -167,6 +167,42 @@ namespace Bloxstrap if (_mustUpgrade) App.Terminate(ErrorCode.ERROR_CANCELLED); } + + private void HandlePrivateChannelLaunch() + { + const string LOG_IDENT = "Bootstrapper::HandlePrivateChannelLaunch"; + + string path = Path.Combine(_latestVersionDirectory, AppData.RobloxInstallerExecutableName); + if (Deployment.PrivateChannel) + { + if (File.Exists(path)) + { + string versionCopyHash = MD5Hash.FromFile(path); + string processHash = MD5Hash.FromFile(Paths.Process); + + if (versionCopyHash != processHash) + { + App.Logger.WriteLine(LOG_IDENT, $"Installer in version directory is not the same as the current process ({versionCopyHash} =/= {processHash})"); + App.Logger.WriteLine(LOG_IDENT, "Copying..."); + File.Copy(Paths.Process, path, true); + } + } + else + { + App.Logger.WriteLine(LOG_IDENT, $"Copying Bloxstrap into the installation folder as {AppData.RobloxInstallerExecutableName}"); + App.Logger.WriteLine(LOG_IDENT, "There is a spy among us..."); + File.Copy(Paths.Process, path); + } + } + else + { + if (File.Exists(path)) + { + App.Logger.WriteLine(LOG_IDENT, $"Deleting {AppData.RobloxInstallerExecutableName} from the version directory"); + File.Delete(path); + } + } + } public async Task Run() { @@ -250,7 +286,9 @@ namespace Bloxstrap if (!_noConnection) { - if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade) + // for private channels, the roblox client will relaunch us with the proper arguments + // so avoid an unnecessary update if roblox is already installed + if ((!Deployment.PrivateChannel && AppData.State.VersionGuid != _latestVersionGuid) || _mustUpgrade) { bool backgroundUpdaterMutexOpen = Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater"); if (App.LaunchSettings.BackgroundUpdaterFlag.Active) @@ -282,6 +320,8 @@ namespace Bloxstrap allModificationsApplied = await ApplyModifications(); } + HandlePrivateChannelLaunch(); + // check registry entries for every launch, just in case the stock bootstrapper changes it back if (IsStudioLaunch) @@ -365,6 +405,12 @@ namespace Bloxstrap { App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}"); + if (ex.StatusCode == HttpStatusCode.Unauthorized) + { + App.Logger.WriteLine(LOG_IDENT, "Enabling private channel launch mode"); + Deployment.PrivateChannel = true; + } + Deployment.Channel = Deployment.DefaultChannel; clientVersion = await Deployment.GetInfo(); } @@ -429,6 +475,12 @@ namespace Bloxstrap return false; } + if (Deployment.PrivateChannel) + { + App.Logger.WriteLine(LOG_IDENT, "Not eligible: Private channel launch"); + return false; + } + // at least 3GB of free space const long minimumFreeSpace = 3_000_000_000; long space = Filesystem.GetFreeDiskSpace(Paths.Base); diff --git a/Bloxstrap/RobloxInterfaces/Deployment.cs b/Bloxstrap/RobloxInterfaces/Deployment.cs index 10fae6c..eb59a22 100644 --- a/Bloxstrap/RobloxInterfaces/Deployment.cs +++ b/Bloxstrap/RobloxInterfaces/Deployment.cs @@ -3,12 +3,17 @@ public static class Deployment { public const string DefaultChannel = "production"; - + private const string VersionStudioHash = "version-012732894899482c"; - public static string Channel = DefaultChannel; + public static string Channel { get; set; } = DefaultChannel; - public static string BinaryType = "WindowsPlayer"; + /// + /// Copies Bloxstrap into the Roblox installation folder to allow for private channel support. + /// + public static bool PrivateChannel { get; set; } = false; + + public static string BinaryType { get; set; } = "WindowsPlayer"; public static bool IsDefaultChannel => Channel.Equals(DefaultChannel, StringComparison.OrdinalIgnoreCase);