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);