mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 01:51:29 -07:00
Merge branch 'dev'
This commit is contained in:
commit
9bba7a8088
@ -1,4 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Web;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
@ -14,7 +15,8 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
public const string ProjectName = "Bloxstrap";
|
public const string ProjectName = "Bloxstrap";
|
||||||
public const string ProjectRepository = "pizzaboxer/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
|
// used only for communicating between app and menu - use Directories.Base for anything else
|
||||||
public static string BaseDirectory = null!;
|
public static string BaseDirectory = null!;
|
||||||
@ -49,7 +51,9 @@ namespace Bloxstrap
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#if RELEASE
|
||||||
private static bool _showingExceptionDialog = false;
|
private static bool _showingExceptionDialog = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
|
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
|
||||||
{
|
{
|
||||||
@ -120,6 +124,10 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
LaunchArgs = e.Args;
|
LaunchArgs = e.Args;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Logger.WriteLine(LOG_IDENT, $"Arguments: {string.Join(' ', LaunchArgs)}");
|
||||||
|
#endif
|
||||||
|
|
||||||
HttpClient.Timeout = TimeSpan.FromSeconds(30);
|
HttpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||||
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
|
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
|
||||||
|
|
||||||
@ -189,6 +197,7 @@ namespace Bloxstrap
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
string commandLine = "";
|
string commandLine = "";
|
||||||
|
LaunchMode? launchMode = null;
|
||||||
|
|
||||||
if (IsMenuLaunch)
|
if (IsMenuLaunch)
|
||||||
{
|
{
|
||||||
@ -216,6 +225,8 @@ namespace Bloxstrap
|
|||||||
if (LaunchArgs[0].StartsWith("roblox-player:"))
|
if (LaunchArgs[0].StartsWith("roblox-player:"))
|
||||||
{
|
{
|
||||||
commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]);
|
commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]);
|
||||||
|
|
||||||
|
launchMode = LaunchMode.Player;
|
||||||
}
|
}
|
||||||
else if (LaunchArgs[0].StartsWith("roblox:"))
|
else if (LaunchArgs[0].StartsWith("roblox:"))
|
||||||
{
|
{
|
||||||
@ -226,25 +237,53 @@ namespace Bloxstrap
|
|||||||
);
|
);
|
||||||
|
|
||||||
commandLine = $"--app --deeplink {LaunchArgs[0]}";
|
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
|
else
|
||||||
{
|
{
|
||||||
commandLine = "--app";
|
commandLine = "--app";
|
||||||
|
|
||||||
|
launchMode = LaunchMode.Player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
commandLine = "--app";
|
commandLine = "--app";
|
||||||
|
|
||||||
|
launchMode = LaunchMode.Player;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(commandLine))
|
if (launchMode != null)
|
||||||
{
|
{
|
||||||
if (!IsFirstRun)
|
if (!IsFirstRun)
|
||||||
ShouldSaveConfigs = true;
|
ShouldSaveConfigs = true;
|
||||||
|
|
||||||
// start bootstrapper and show the bootstrapper modal if we're not running silently
|
// start bootstrapper and show the bootstrapper modal if we're not running silently
|
||||||
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
|
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
|
||||||
Bootstrapper bootstrapper = new(commandLine);
|
Bootstrapper bootstrapper = new(commandLine, (LaunchMode)launchMode);
|
||||||
IBootstrapperDialog? dialog = null;
|
IBootstrapperDialog? dialog = null;
|
||||||
|
|
||||||
if (!IsQuiet)
|
if (!IsQuiet)
|
||||||
@ -261,7 +300,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
Mutex? singletonMutex = null;
|
Mutex? singletonMutex = null;
|
||||||
|
|
||||||
if (Settings.Prop.MultiInstanceLaunching)
|
if (Settings.Prop.MultiInstanceLaunching && launchMode == LaunchMode.Player)
|
||||||
{
|
{
|
||||||
Logger.WriteLine(LOG_IDENT, "Creating singleton mutex");
|
Logger.WriteLine(LOG_IDENT, "Creating singleton mutex");
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
|
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
|
||||||
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -10,38 +10,8 @@ namespace Bloxstrap
|
|||||||
public class Bootstrapper
|
public class Bootstrapper
|
||||||
{
|
{
|
||||||
#region Properties
|
#region Properties
|
||||||
// in case a new package is added, you can find the corresponding directory
|
private const int ProgressBarMaximum = 10000;
|
||||||
// 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<string, string> PackageDirectories = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "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 =
|
private const string AppSettings =
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
|
||||||
"<Settings>\r\n" +
|
"<Settings>\r\n" +
|
||||||
@ -51,15 +21,49 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
private readonly CancellationTokenSource _cancelTokenSource = new();
|
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 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 string _latestVersionGuid = null!;
|
||||||
private PackageManifest _versionPackageManifest = null!;
|
private PackageManifest _versionPackageManifest = null!;
|
||||||
private FileManifest _versionFileManifest = null!;
|
|
||||||
private string _versionFolder = null!;
|
private string _versionFolder = null!;
|
||||||
|
|
||||||
private bool _isInstalling = false;
|
private bool _isInstalling = false;
|
||||||
@ -68,19 +72,33 @@ namespace Bloxstrap
|
|||||||
private int _packagesExtracted = 0;
|
private int _packagesExtracted = 0;
|
||||||
private bool _cancelFired = false;
|
private bool _cancelFired = false;
|
||||||
|
|
||||||
|
private IReadOnlyDictionary<string, string> _packageDirectories;
|
||||||
|
|
||||||
public IBootstrapperDialog? Dialog = null;
|
public IBootstrapperDialog? Dialog = null;
|
||||||
|
|
||||||
|
public bool IsStudioLaunch => _launchMode != LaunchMode.Player;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Core
|
#region Core
|
||||||
public Bootstrapper(string launchCommandLine)
|
public Bootstrapper(string launchCommandLine, LaunchMode launchMode)
|
||||||
{
|
{
|
||||||
_launchCommandLine = launchCommandLine;
|
_launchCommandLine = launchCommandLine;
|
||||||
|
_launchMode = launchMode;
|
||||||
|
|
||||||
|
_packageDirectories = _launchMode == LaunchMode.Player ? PackageMap.Player : PackageMap.Studio;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetStatus(string message)
|
private void SetStatus(string message)
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine("Bootstrapper::SetStatus", message);
|
App.Logger.WriteLine("Bootstrapper::SetStatus", message);
|
||||||
|
|
||||||
|
string productName = "Roblox";
|
||||||
|
|
||||||
|
if (_launchMode != LaunchMode.Player)
|
||||||
|
productName += " Studio";
|
||||||
|
|
||||||
|
message = message.Replace("{product}", productName);
|
||||||
|
|
||||||
// yea idk
|
// yea idk
|
||||||
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog)
|
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog)
|
||||||
message = message.Replace("...", "");
|
message = message.Replace("...", "");
|
||||||
@ -91,15 +109,16 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
private void UpdateProgressBar()
|
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
|
// 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
|
// too lazy to fix properly so lol
|
||||||
if (newProgress > 100)
|
progressValue = Math.Clamp(progressValue, 0, ProgressBarMaximum);
|
||||||
return;
|
|
||||||
|
|
||||||
if (Dialog is not null)
|
Dialog.ProgressValue = progressValue;
|
||||||
Dialog.ProgressValue = newProgress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Run()
|
public async Task Run()
|
||||||
@ -183,7 +202,7 @@ namespace Bloxstrap
|
|||||||
await CheckLatestVersion();
|
await CheckLatestVersion();
|
||||||
|
|
||||||
// install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist
|
// 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();
|
await InstallLatestVersion();
|
||||||
|
|
||||||
if (App.IsFirstRun)
|
if (App.IsFirstRun)
|
||||||
@ -223,18 +242,20 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
ClientVersion clientVersion;
|
ClientVersion clientVersion;
|
||||||
|
|
||||||
|
string binaryType = _launchMode == LaunchMode.Player ? "WindowsPlayer" : "WindowsStudio64";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel);
|
clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType);
|
||||||
}
|
}
|
||||||
catch (HttpResponseException ex)
|
catch (HttpResponseException ex)
|
||||||
{
|
{
|
||||||
if (ex.ResponseMessage.StatusCode != HttpStatusCode.NotFound)
|
if (ex.ResponseMessage.StatusCode != HttpStatusCode.NotFound)
|
||||||
throw;
|
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;
|
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)
|
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.Logger.WriteLine("Bootstrapper::CheckLatestVersion", $"Changed Roblox channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}");
|
||||||
|
|
||||||
App.Settings.Prop.Channel = 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;
|
_latestVersionGuid = clientVersion.VersionGuid;
|
||||||
_versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid);
|
_versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid);
|
||||||
_versionPackageManifest = await PackageManifest.Get(_latestVersionGuid);
|
_versionPackageManifest = await PackageManifest.Get(_latestVersionGuid);
|
||||||
_versionFileManifest = await FileManifest.Get(_latestVersionGuid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartRoblox()
|
private async Task StartRoblox()
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::StartRoblox";
|
const string LOG_IDENT = "Bootstrapper::StartRoblox";
|
||||||
|
|
||||||
SetStatus("Starting Roblox...");
|
SetStatus("Starting {product}...");
|
||||||
|
|
||||||
if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch)
|
if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch)
|
||||||
{
|
{
|
||||||
@ -294,21 +314,38 @@ namespace Bloxstrap
|
|||||||
return;
|
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())
|
if (App.Settings.Prop.Channel.ToLowerInvariant() == RobloxDeployment.DefaultChannel.ToLowerInvariant())
|
||||||
_launchCommandLine += "production";
|
_launchCommandLine += "production";
|
||||||
else
|
else
|
||||||
_launchCommandLine += App.Settings.Prop.Channel.ToLowerInvariant();
|
_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
|
// whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes
|
||||||
bool shouldWait = false;
|
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
|
// 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;
|
int gameClientPid;
|
||||||
using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine))
|
using (Process gameClient = Process.Start(startInfo)!)
|
||||||
{
|
{
|
||||||
gameClientPid = gameClient.Id;
|
gameClientPid = gameClient.Id;
|
||||||
}
|
}
|
||||||
@ -319,7 +356,8 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid})");
|
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();
|
bool startEventFired = await startEvent.WaitForEvent();
|
||||||
|
|
||||||
@ -329,6 +367,9 @@ namespace Bloxstrap
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (App.Settings.Prop.EnableActivityTracking && _launchMode == LaunchMode.Player)
|
||||||
|
App.NotifyIcon?.SetProcessId(gameClientPid);
|
||||||
|
|
||||||
if (App.Settings.Prop.EnableActivityTracking)
|
if (App.Settings.Prop.EnableActivityTracking)
|
||||||
{
|
{
|
||||||
activityWatcher = new();
|
activityWatcher = new();
|
||||||
@ -476,7 +517,10 @@ namespace Bloxstrap
|
|||||||
using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}");
|
using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}");
|
||||||
|
|
||||||
// sum compressed and uncompressed package sizes and convert to kilobytes
|
// 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);
|
uninstallKey.SetValue("EstimatedSize", totalSize);
|
||||||
|
|
||||||
@ -495,14 +539,26 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
ProtocolHandler.Register("roblox", "Roblox", Paths.Application);
|
ProtocolHandler.Register("roblox", "Roblox", Paths.Application);
|
||||||
ProtocolHandler.Register("roblox-player", "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
|
ProtocolHandler.RegisterRobloxPlace(Paths.Application);
|
||||||
if (File.Exists(Paths.Application) && App.IsFirstRun)
|
ProtocolHandler.RegisterExtension(".rbxl");
|
||||||
File.Delete(Paths.Application);
|
ProtocolHandler.RegisterExtension(".rbxlx");
|
||||||
|
|
||||||
// check to make sure bootstrapper is in the install folder
|
if (Environment.ProcessPath is not null && Environment.ProcessPath != Paths.Application)
|
||||||
if (!File.Exists(Paths.Application) && Environment.ProcessPath is not null)
|
{
|
||||||
File.Copy(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(),
|
// this SHOULD go under Register(),
|
||||||
// but then people who have Bloxstrap v1.0.0 installed won't have this without a reinstall
|
// 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, "", 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, "-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)
|
if (App.Settings.Prop.CreateDesktopIcon)
|
||||||
{
|
{
|
||||||
@ -630,7 +687,7 @@ namespace Bloxstrap
|
|||||||
const string LOG_IDENT = "Bootstrapper::Uninstall";
|
const string LOG_IDENT = "Bootstrapper::Uninstall";
|
||||||
|
|
||||||
// prompt to shutdown roblox if its currently running
|
// 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");
|
App.Logger.WriteLine(LOG_IDENT, $"Prompting to shut down all open Roblox instances");
|
||||||
|
|
||||||
@ -645,7 +702,13 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
try
|
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.CloseMainWindow();
|
||||||
process.Close();
|
process.Close();
|
||||||
@ -662,16 +725,17 @@ namespace Bloxstrap
|
|||||||
SetStatus($"Uninstalling {App.ProjectName}...");
|
SetStatus($"Uninstalling {App.ProjectName}...");
|
||||||
|
|
||||||
App.ShouldSaveConfigs = false;
|
App.ShouldSaveConfigs = false;
|
||||||
bool robloxStillInstalled = true;
|
bool robloxPlayerStillInstalled = true;
|
||||||
|
bool robloxStudioStillInstalled = true;
|
||||||
|
|
||||||
// check if stock bootstrapper is still installed
|
// 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)
|
if (bootstrapperKey is null)
|
||||||
{
|
{
|
||||||
|
robloxPlayerStillInstalled = false;
|
||||||
|
|
||||||
ProtocolHandler.Unregister("roblox");
|
ProtocolHandler.Unregister("roblox");
|
||||||
ProtocolHandler.Unregister("roblox-player");
|
ProtocolHandler.Unregister("roblox-player");
|
||||||
|
|
||||||
robloxStillInstalled = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio") is not null;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -683,6 +747,27 @@ namespace Bloxstrap
|
|||||||
ProtocolHandler.Register("roblox-player", "Roblox", bootstrapperLocation);
|
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
|
// 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)
|
// 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
|
// 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");
|
string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox");
|
||||||
|
|
||||||
if (!robloxStillInstalled && Directory.Exists(robloxFolder))
|
if (!robloxPlayerStillInstalled && !robloxStudioStillInstalled && Directory.Exists(robloxFolder))
|
||||||
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
|
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
|
||||||
|
|
||||||
foreach (var process in cleanupSequence)
|
foreach (var process in cleanupSequence)
|
||||||
@ -767,7 +852,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
_isInstalling = true;
|
_isInstalling = true;
|
||||||
|
|
||||||
SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox...");
|
SetStatus(FreshInstall ? "Installing {product}..." : "Upgrading {product}...");
|
||||||
|
|
||||||
Directory.CreateDirectory(Paths.Base);
|
Directory.CreateDirectory(Paths.Base);
|
||||||
Directory.CreateDirectory(Paths.Downloads);
|
Directory.CreateDirectory(Paths.Downloads);
|
||||||
@ -793,10 +878,12 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
Dialog.CancelEnabled = true;
|
Dialog.CancelEnabled = true;
|
||||||
Dialog.ProgressStyle = ProgressBarStyle.Continuous;
|
Dialog.ProgressStyle = ProgressBarStyle.Continuous;
|
||||||
}
|
|
||||||
|
|
||||||
// compute total bytes to download
|
Dialog.ProgressMaximum = ProgressBarMaximum;
|
||||||
_progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize);
|
|
||||||
|
// compute total bytes to download
|
||||||
|
_progressIncrement = (double)ProgressBarMaximum / _versionPackageManifest.Sum(package => package.PackedSize);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Package package in _versionPackageManifest)
|
foreach (Package package in _versionPackageManifest)
|
||||||
{
|
{
|
||||||
@ -812,7 +899,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
// extract the package immediately after download asynchronously
|
// extract the package immediately after download asynchronously
|
||||||
// discard is just used to suppress the warning
|
// 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)
|
if (_cancelFired)
|
||||||
@ -824,7 +911,7 @@ namespace Bloxstrap
|
|||||||
if (Dialog is not null)
|
if (Dialog is not null)
|
||||||
{
|
{
|
||||||
Dialog.ProgressStyle = ProgressBarStyle.Marquee;
|
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
|
// 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
|
// move old compatibility flags for the old location
|
||||||
using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"))
|
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);
|
string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation);
|
||||||
|
|
||||||
if (appFlags is not null)
|
if (appFlags is not null)
|
||||||
@ -876,34 +963,34 @@ namespace Bloxstrap
|
|||||||
appFlagsKey.DeleteValue(oldGameClientLocation);
|
appFlagsKey.DeleteValue(oldGameClientLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// delete any old version folders
|
_versionGuid = _latestVersionGuid;
|
||||||
// we only do this if roblox isnt running just in case an update happened
|
|
||||||
// while they were launching a second instance or something idk
|
// delete any old version folders
|
||||||
if (!Process.GetProcessesByName(App.RobloxAppName).Any())
|
// 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-"))
|
dir.Delete(true);
|
||||||
continue;
|
}
|
||||||
|
catch (Exception ex)
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Removing old version folder for {dir.Name}");
|
{
|
||||||
|
App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!");
|
||||||
try
|
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
|
// don't register program size until the program is registered, which will be done after this
|
||||||
if (!App.IsFirstRun && !FreshInstall)
|
if (!App.IsFirstRun && !FreshInstall)
|
||||||
RegisterProgramSize();
|
RegisterProgramSize();
|
||||||
@ -990,7 +1077,7 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::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");
|
App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check");
|
||||||
return;
|
return;
|
||||||
@ -1182,6 +1269,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!);
|
||||||
|
|
||||||
|
Filesystem.AssertReadOnly(fileVersionFolder);
|
||||||
File.Copy(fileModFolder, fileVersionFolder, true);
|
File.Copy(fileModFolder, fileVersionFolder, true);
|
||||||
Filesystem.AssertReadOnly(fileVersionFolder);
|
Filesystem.AssertReadOnly(fileVersionFolder);
|
||||||
|
|
||||||
@ -1196,7 +1284,7 @@ namespace Bloxstrap
|
|||||||
if (modFolderFiles.Contains(fileLocation))
|
if (modFolderFiles.Contains(fileLocation))
|
||||||
continue;
|
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
|
// package doesn't exist, likely mistakenly placed file
|
||||||
if (String.IsNullOrEmpty(package.Key))
|
if (String.IsNullOrEmpty(package.Key))
|
||||||
@ -1361,9 +1449,11 @@ namespace Bloxstrap
|
|||||||
_totalDownloadedBytes += bytesRead;
|
_totalDownloadedBytes += bytesRead;
|
||||||
UpdateProgressBar();
|
UpdateProgressBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MD5Hash.FromStream(fileStream) != package.Signature)
|
string hash = MD5Hash.FromStream(fileStream);
|
||||||
throw new Exception("Signature does not match!");
|
|
||||||
|
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)");
|
App.Logger.WriteLine(LOG_IDENT, $"Finished downloading! ({totalBytesRead} bytes total)");
|
||||||
break;
|
break;
|
||||||
@ -1373,12 +1463,12 @@ namespace Bloxstrap
|
|||||||
App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})");
|
App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})");
|
||||||
App.Logger.WriteException(LOG_IDENT, ex);
|
App.Logger.WriteException(LOG_IDENT, ex);
|
||||||
|
|
||||||
|
if (i >= maxTries || ex.GetType() == typeof(ChecksumFailedException))
|
||||||
|
throw;
|
||||||
|
|
||||||
if (File.Exists(packageLocation))
|
if (File.Exists(packageLocation))
|
||||||
File.Delete(packageLocation);
|
File.Delete(packageLocation);
|
||||||
|
|
||||||
if (i >= maxTries)
|
|
||||||
throw;
|
|
||||||
|
|
||||||
_totalDownloadedBytes -= totalBytesRead;
|
_totalDownloadedBytes -= totalBytesRead;
|
||||||
UpdateProgressBar();
|
UpdateProgressBar();
|
||||||
|
|
||||||
@ -1394,75 +1484,26 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExtractPackage(Package package)
|
private Task ExtractPackage(Package package)
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "Bootstrapper::ExtractPackage";
|
const string LOG_IDENT = "Bootstrapper::ExtractPackage";
|
||||||
|
|
||||||
if (_cancelFired)
|
if (_cancelFired)
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
string packageLocation = Path.Combine(Paths.Downloads, package.Signature);
|
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));
|
var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip();
|
||||||
|
fastZip.ExtractZip(packageLocation, packageFolder, null);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}");
|
App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}");
|
||||||
|
|
||||||
_packagesExtracted += 1;
|
_packagesExtracted += 1;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExtractFileFromPackage(string packageName, string fileName)
|
private async Task ExtractFileFromPackage(string packageName, string fileName)
|
||||||
@ -1481,7 +1522,7 @@ namespace Bloxstrap
|
|||||||
if (entry is null)
|
if (entry is null)
|
||||||
return;
|
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);
|
entry.ExtractToFile(extractionPath, true);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
9
Bloxstrap/Enums/LaunchMode.cs
Normal file
9
Bloxstrap/Enums/LaunchMode.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Bloxstrap.Enums
|
||||||
|
{
|
||||||
|
public enum LaunchMode
|
||||||
|
{
|
||||||
|
Player,
|
||||||
|
Studio,
|
||||||
|
StudioAuth
|
||||||
|
}
|
||||||
|
}
|
15
Bloxstrap/Exceptions/ChecksumFailedException.cs
Normal file
15
Bloxstrap/Exceptions/ChecksumFailedException.cs
Normal file
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
// this is the value of the 'FStringPartTexturePackTablePre2022' flag
|
// 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 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<string, string> PresetFlags = new Dictionary<string, string>
|
public static IReadOnlyDictionary<string, string> PresetFlags = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
@ -25,9 +26,11 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
{ "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" },
|
{ "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" },
|
||||||
{ "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" },
|
{ "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" },
|
||||||
{ "Rendering.TexturePack", "FStringPartTexturePackTable2022" },
|
|
||||||
{ "Rendering.DisableScaling", "DFFlagDisableDPIScale" },
|
{ "Rendering.DisableScaling", "DFFlagDisableDPIScale" },
|
||||||
|
|
||||||
|
{ "Rendering.Materials.NewTexturePack", "FStringPartTexturePackTable2022" },
|
||||||
|
{ "Rendering.Materials.OldTexturePack", "FStringPartTexturePackTablePre2022" },
|
||||||
|
|
||||||
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
|
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
|
||||||
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
|
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
|
||||||
{ "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" },
|
{ "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" },
|
||||||
@ -80,6 +83,13 @@ namespace Bloxstrap
|
|||||||
{ "8x MSAA", "8" }
|
{ "8x MSAA", "8" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, string> MaterialVersions => new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "Chosen by game", "None" },
|
||||||
|
{ "Old (Pre-2022)", "NewTexturePack" },
|
||||||
|
{ "New (2022)", "OldTexturePack" }
|
||||||
|
};
|
||||||
|
|
||||||
// this is one hell of a dictionary definition lmao
|
// 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?
|
// since these all set the same flags, wouldn't making this use bitwise operators be better?
|
||||||
public static IReadOnlyDictionary<string, Dictionary<string, string?>> IGMenuVersions => new Dictionary<string, Dictionary<string, string?>>
|
public static IReadOnlyDictionary<string, Dictionary<string, string?>> IGMenuVersions => new Dictionary<string, Dictionary<string, string?>>
|
||||||
|
@ -3,7 +3,15 @@
|
|||||||
public class State
|
public class State
|
||||||
{
|
{
|
||||||
public string LastEnrolledChannel { get; set; } = "";
|
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<string> ModManifest { get; set; } = new();
|
public List<string> ModManifest { get; set; } = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
Bloxstrap/PackageMap.cs
Normal file
88
Bloxstrap/PackageMap.cs
Normal file
@ -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<string, string> Player
|
||||||
|
{
|
||||||
|
get { return CombineDictionaries(_common, _playerOnly); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, string> 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<string, string> _common = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "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<string, string> _playerOnly = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "RobloxApp.zip", @"" }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, string> _studioOnly = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "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<string, string> CombineDictionaries(IReadOnlyDictionary<string, string> d1, IReadOnlyDictionary<string, string> d2)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> newD = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var d in d1)
|
||||||
|
newD[d.Key] = d.Value;
|
||||||
|
|
||||||
|
foreach (var d in d2)
|
||||||
|
newD[d.Key] = d.Value;
|
||||||
|
|
||||||
|
return newD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,10 @@
|
|||||||
"Bloxstrap (Deeplink)": {
|
"Bloxstrap (Deeplink)": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"commandLineArgs": "roblox://experiences/start?placeId=95206881"
|
"commandLineArgs": "roblox://experiences/start?placeId=95206881"
|
||||||
|
},
|
||||||
|
"Bloxstrap (Studio Launch)": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "-ide"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,8 @@ namespace Bloxstrap
|
|||||||
{
|
{
|
||||||
static class ProtocolHandler
|
static class ProtocolHandler
|
||||||
{
|
{
|
||||||
|
private const string RobloxPlaceKey = "Roblox.Place";
|
||||||
|
|
||||||
// map uri keys to command line args
|
// map uri keys to command line args
|
||||||
private static readonly IReadOnlyDictionary<string, string> UriKeyArgMap = new Dictionary<string, string>()
|
private static readonly IReadOnlyDictionary<string, string> UriKeyArgMap = new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
@ -18,7 +20,12 @@ namespace Bloxstrap
|
|||||||
{ "browsertrackerid", "-b " },
|
{ "browsertrackerid", "-b " },
|
||||||
{ "robloxLocale", "--rloc " },
|
{ "robloxLocale", "--rloc " },
|
||||||
{ "gameLocale", "--gloc " },
|
{ "gameLocale", "--gloc " },
|
||||||
{ "channel", "-channel " }
|
{ "channel", "-channel " },
|
||||||
|
// studio
|
||||||
|
{ "task", "-task " },
|
||||||
|
{ "placeId", "-placeId " },
|
||||||
|
{ "universeId", "-universeId " },
|
||||||
|
{ "userId", "-userId " }
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ParseUri(string protocol)
|
public static string ParseUri(string protocol)
|
||||||
@ -108,9 +115,10 @@ namespace Bloxstrap
|
|||||||
public static void Register(string key, string name, string handler)
|
public static void Register(string key, string name, string handler)
|
||||||
{
|
{
|
||||||
string handlerArgs = $"\"{handler}\" %1";
|
string handlerArgs = $"\"{handler}\" %1";
|
||||||
RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
|
|
||||||
RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon");
|
using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
|
||||||
RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command");
|
using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon");
|
||||||
|
using RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command");
|
||||||
|
|
||||||
if (uriKey.GetValue("") is null)
|
if (uriKey.GetValue("") is null)
|
||||||
{
|
{
|
||||||
@ -118,15 +126,44 @@ namespace Bloxstrap
|
|||||||
uriKey.SetValue("URL Protocol", "");
|
uriKey.SetValue("URL Protocol", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((string?)uriCommandKey.GetValue("") != handlerArgs)
|
if (uriCommandKey.GetValue("") as string != handlerArgs)
|
||||||
{
|
{
|
||||||
uriIconKey.SetValue("", handler);
|
uriIconKey.SetValue("", handler);
|
||||||
uriCommandKey.SetValue("", handlerArgs);
|
uriCommandKey.SetValue("", handlerArgs);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uriKey.Close();
|
public static void RegisterRobloxPlace(string handler)
|
||||||
uriIconKey.Close();
|
{
|
||||||
uriCommandKey.Close();
|
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)
|
public static void Unregister(string key)
|
||||||
|
@ -74,22 +74,23 @@
|
|||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<ClientVersion> GetInfo(string channel, bool extraInformation = false)
|
public static async Task<ClientVersion> GetInfo(string channel, bool extraInformation = false, string binaryType = "WindowsPlayer")
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "RobloxDeployment::GetInfo";
|
const string LOG_IDENT = "RobloxDeployment::GetInfo";
|
||||||
|
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel} (extraInformation={extraInformation})");
|
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel} (extraInformation={extraInformation})");
|
||||||
|
|
||||||
|
string cacheKey = $"{channel}-{binaryType}";
|
||||||
ClientVersion clientVersion;
|
ClientVersion clientVersion;
|
||||||
|
|
||||||
if (ClientVersionCache.ContainsKey(channel))
|
if (ClientVersionCache.ContainsKey(cacheKey))
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Deploy information is cached");
|
App.Logger.WriteLine(LOG_IDENT, "Deploy information is cached");
|
||||||
clientVersion = ClientVersionCache[channel];
|
clientVersion = ClientVersionCache[cacheKey];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string path = $"/v2/client-version/WindowsPlayer/channel/{channel}";
|
string path = $"/v2/client-version/{binaryType}/channel/{channel}";
|
||||||
HttpResponseMessage deployInfoResponse;
|
HttpResponseMessage deployInfoResponse;
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -152,7 +153,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientVersionCache[channel] = clientVersion;
|
ClientVersionCache[cacheKey] = clientVersion;
|
||||||
|
|
||||||
return clientVersion;
|
return clientVersion;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base
|
|||||||
protected virtual string _message { get; set; } = "Please wait...";
|
protected virtual string _message { get; set; } = "Please wait...";
|
||||||
protected virtual ProgressBarStyle _progressStyle { get; set; }
|
protected virtual ProgressBarStyle _progressStyle { get; set; }
|
||||||
protected virtual int _progressValue { get; set; }
|
protected virtual int _progressValue { get; set; }
|
||||||
|
protected virtual int _progressMaximum { get; set; }
|
||||||
protected virtual bool _cancelEnabled { get; set; }
|
protected virtual bool _cancelEnabled { get; set; }
|
||||||
|
|
||||||
public string Message
|
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
|
public int ProgressValue
|
||||||
{
|
{
|
||||||
get => _progressValue;
|
get => _progressValue;
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<ScaleTransform ScaleY="0.9"/>
|
<ScaleTransform ScaleY="0.9"/>
|
||||||
</TextBlock.LayoutTransform>
|
</TextBlock.LayoutTransform>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<ProgressBar Grid.Row="1" Width="480" Height="12" Foreground="{Binding Foreground}" Background="{Binding ProgressBarBackground}" BorderThickness="0" IsIndeterminate="{Binding ProgressIndeterminate}" Value="{Binding ProgressValue}"></ProgressBar>
|
<ProgressBar Grid.Row="1" Width="480" Height="12" Foreground="{Binding Foreground}" Background="{Binding ProgressBarBackground}" BorderThickness="0" IsIndeterminate="{Binding ProgressIndeterminate}" Maximum="{Binding ProgressMaximum, Mode=OneWay}" Value="{Binding ProgressValue}"></ProgressBar>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
@ -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
|
public int ProgressValue
|
||||||
{
|
{
|
||||||
get => _viewModel.ProgressValue;
|
get => _viewModel.ProgressValue;
|
||||||
@ -69,7 +79,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
|
|
||||||
public ByfronDialog()
|
public ByfronDialog()
|
||||||
{
|
{
|
||||||
_viewModel = new ByfronDialogViewModel(this);
|
string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false);
|
||||||
|
_viewModel = new ByfronDialogViewModel(this, version);
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
Title = App.Settings.Prop.BootstrapperTitle;
|
Title = App.Settings.Prop.BootstrapperTitle;
|
||||||
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
|
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<StackPanel Grid.Column="1">
|
<StackPanel Grid.Column="1">
|
||||||
<TextBlock Margin="16,8,0,0" FontSize="20" Text="{Binding Message, Mode=OneWay}" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
<TextBlock Margin="16,8,0,0" FontSize="20" Text="{Binding Message, Mode=OneWay}" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||||
<ProgressBar Margin="16,16,0,16" IsIndeterminate="{Binding ProgressIndeterminate, Mode=OneWay}" Value="{Binding ProgressValue, Mode=OneWay}" />
|
<ProgressBar Margin="16,16,0,16" IsIndeterminate="{Binding ProgressIndeterminate, Mode=OneWay}" Maximum="{Binding ProgressMaximum, Mode=OneWay}" Value="{Binding ProgressValue, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
@ -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
|
public int ProgressValue
|
||||||
{
|
{
|
||||||
get => _viewModel.ProgressValue;
|
get => _viewModel.ProgressValue;
|
||||||
|
@ -21,6 +21,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
set => ProgressBar.Style = value;
|
set => ProgressBar.Style = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override int _progressMaximum
|
||||||
|
{
|
||||||
|
get => ProgressBar.Maximum;
|
||||||
|
set => ProgressBar.Maximum = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override int _progressValue
|
protected override int _progressValue
|
||||||
{
|
{
|
||||||
get => ProgressBar.Value;
|
get => ProgressBar.Value;
|
||||||
|
@ -20,6 +20,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
set => ProgressBar.Style = value;
|
set => ProgressBar.Style = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override int _progressMaximum
|
||||||
|
{
|
||||||
|
get => ProgressBar.Maximum;
|
||||||
|
set => ProgressBar.Maximum = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override int _progressValue
|
protected override int _progressValue
|
||||||
{
|
{
|
||||||
get => ProgressBar.Value;
|
get => ProgressBar.Value;
|
||||||
|
@ -21,6 +21,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
|
|||||||
set => ProgressBar.Style = value;
|
set => ProgressBar.Style = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override int _progressMaximum
|
||||||
|
{
|
||||||
|
get => ProgressBar.Maximum;
|
||||||
|
set => ProgressBar.Maximum = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override int _progressValue
|
protected override int _progressValue
|
||||||
{
|
{
|
||||||
get => ProgressBar.Value;
|
get => ProgressBar.Value;
|
||||||
|
@ -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
|
protected sealed override int _progressValue
|
||||||
{
|
{
|
||||||
get => _dialogPage.ProgressBar?.Value ?? 0;
|
get => _dialogPage.ProgressBar?.Value ?? 0;
|
||||||
|
@ -60,6 +60,18 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</MenuItem.Header>
|
</MenuItem.Header>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem x:Name="CloseRobloxMenuItem" Visibility="Collapsed" Click="CloseRobloxMenuItem_Click">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="24" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:SymbolIcon Grid.Column="0" Symbol="WindowHeaderHorizontalOff20"/>
|
||||||
|
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="Close Roblox" />
|
||||||
|
</Grid>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem x:Name="LogTracerMenuItem" Header="Open log tracer" Visibility="Collapsed" Click="LogTracerMenuItem_Click" />
|
<MenuItem x:Name="LogTracerMenuItem" Header="Open log tracer" Visibility="Collapsed" Click="LogTracerMenuItem_Click" />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</ui:UiWindow.ContextMenu>
|
</ui:UiWindow.ContextMenu>
|
||||||
|
@ -26,14 +26,16 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
|||||||
|
|
||||||
private LogTracer? _logTracerWindow;
|
private LogTracer? _logTracerWindow;
|
||||||
private ServerInformation? _serverInformationWindow;
|
private ServerInformation? _serverInformationWindow;
|
||||||
|
private int? _processId;
|
||||||
|
|
||||||
public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler)
|
public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler, int? processId)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
ApplyTheme();
|
ApplyTheme();
|
||||||
|
|
||||||
_activityWatcher = activityWatcher;
|
_activityWatcher = activityWatcher;
|
||||||
_richPresenceHandler = richPresenceHandler;
|
_richPresenceHandler = richPresenceHandler;
|
||||||
|
_processId = processId;
|
||||||
|
|
||||||
if (_activityWatcher is not null)
|
if (_activityWatcher is not null)
|
||||||
{
|
{
|
||||||
@ -47,6 +49,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
|||||||
if (_richPresenceHandler is not null)
|
if (_richPresenceHandler is not null)
|
||||||
RichPresenceMenuItem.Visibility = Visibility.Visible;
|
RichPresenceMenuItem.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
if (_processId is not null)
|
||||||
|
CloseRobloxMenuItem.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
|
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,5 +123,21 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
|||||||
|
|
||||||
_logTracerWindow.Activate();
|
_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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,9 +221,15 @@
|
|||||||
<TextBlock Margin="0,2,0,0" FontSize="12" Text="MIT License" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="MIT License" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</ui:CardAction>
|
||||||
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
|
<ui:CardAction Grid.Row="1" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock FontSize="14" Text="Roblox Studio Mod Manager by MaximumADHD" />
|
<TextBlock FontSize="13" Text="RSMM by MaximumADHD" />
|
||||||
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="MIT License" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:CardAction>
|
||||||
|
<ui:CardAction Grid.Row="1" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock FontSize="13" Text="SharpZipLib by icsharpcode" />
|
||||||
<TextBlock Margin="0,2,0,0" FontSize="12" Text="MIT License" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="MIT License" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</ui:CardAction>
|
||||||
|
@ -150,17 +150,17 @@
|
|||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Grid.Column="0" FontSize="14" Text="Use old material textures" />
|
<TextBlock Grid.Column="0" FontSize="14" Text="Preferred materials" />
|
||||||
<TextBlock Grid.Column="1" Margin="4,0,0,0">
|
<TextBlock Grid.Column="1" Margin="4,0,0,0">
|
||||||
<Hyperlink TextDecorations="None" ToolTip="More information on this preset" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#old-material-textures">
|
<Hyperlink TextDecorations="None" ToolTip="More information on this preset" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#old-material-textures">
|
||||||
<ui:SymbolIcon Symbol="QuestionCircle48" Margin="0,1,0,0" />
|
<ui:SymbolIcon Symbol="QuestionCircle48" Margin="0,1,0,0" />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Toggle whether to use the old material textures used prior to 2022." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Choose which material version should be forced in all games." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardControl.Header>
|
</ui:CardControl.Header>
|
||||||
<ui:ToggleSwitch IsChecked="{Binding Pre2022TexturesEnabled, Mode=TwoWay}" />
|
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding MaterialVersions.Keys, Mode=OneTime}" Text="{Binding SelectedMaterialVersion, Mode=TwoWay}" />
|
||||||
</ui:CardControl>
|
</ui:CardControl>
|
||||||
<ui:CardControl Margin="0,8,0,0">
|
<ui:CardControl Margin="0,8,0,0">
|
||||||
<ui:CardControl.Header>
|
<ui:CardControl.Header>
|
||||||
|
@ -121,33 +121,31 @@
|
|||||||
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding EmojiTypes.Keys, Mode=OneTime}" Text="{Binding SelectedEmojiType, Mode=TwoWay}" />
|
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding EmojiTypes.Keys, Mode=OneTime}" Text="{Binding SelectedEmojiType, Mode=TwoWay}" />
|
||||||
</ui:CardControl>
|
</ui:CardControl>
|
||||||
|
|
||||||
<StackPanel x:Name="MiscellaneousOptions">
|
<TextBlock Text="Miscellaneous" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
|
||||||
<TextBlock Text="Miscellaneous" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
|
<ui:CardControl Margin="0,8,0,0">
|
||||||
<ui:CardControl Margin="0,8,0,0">
|
<ui:CardControl.Header>
|
||||||
<ui:CardControl.Header>
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock FontSize="14" Text="Apply custom font" />
|
|
||||||
<TextBlock Margin="0,2,0,0" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}">
|
|
||||||
Forces every in-game font to be a font that you choose.
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</ui:CardControl.Header>
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<ui:Button Icon="DocumentAdd16" Content="Choose font..." Command="{Binding ManageCustomFontCommand}" Visibility="{Binding ChooseCustomFontVisibility, Mode=OneWay}" />
|
<TextBlock FontSize="14" Text="Apply custom font" />
|
||||||
<ui:Button Icon="Delete16" Content="Remove applied font" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}">
|
||||||
|
Forces every in-game font to be a font that you choose.
|
||||||
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardControl>
|
</ui:CardControl.Header>
|
||||||
<ui:CardControl Margin="0,8,0,0">
|
<StackPanel>
|
||||||
<ui:CardControl.Header>
|
<ui:Button Icon="DocumentAdd16" Content="Choose font..." Command="{Binding ManageCustomFontCommand}" Visibility="{Binding ChooseCustomFontVisibility, Mode=OneWay}" />
|
||||||
<StackPanel>
|
<ui:Button Icon="Delete16" Content="Remove applied font" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" />
|
||||||
<TextBlock FontSize="14" Text="Disable fullscreen optimizations" />
|
</StackPanel>
|
||||||
<TextBlock Margin="0,2,0,0" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}">
|
</ui:CardControl>
|
||||||
A Windows feature that intends to improve fullscreen performance. <Hyperlink Foreground="{DynamicResource TextFillColorPrimaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://devblogs.microsoft.com/directx/demystifying-full-screen-optimizations/">See here for more information</Hyperlink>.
|
<ui:CardControl x:Name="FullscreenOptimizationsToggle" Margin="0,8,0,0">
|
||||||
</TextBlock>
|
<ui:CardControl.Header>
|
||||||
</StackPanel>
|
<StackPanel>
|
||||||
</ui:CardControl.Header>
|
<TextBlock FontSize="14" Text="Disable fullscreen optimizations" />
|
||||||
<ui:ToggleSwitch IsChecked="{Binding DisableFullscreenOptimizations, Mode=TwoWay}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}">
|
||||||
</ui:CardControl>
|
A Windows feature that intends to improve fullscreen performance. <Hyperlink Foreground="{DynamicResource TextFillColorPrimaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://devblogs.microsoft.com/directx/demystifying-full-screen-optimizations/">See here for more information</Hyperlink>.
|
||||||
</StackPanel>
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</ui:CardControl.Header>
|
||||||
|
<ui:ToggleSwitch IsChecked="{Binding DisableFullscreenOptimizations, Mode=TwoWay}" />
|
||||||
|
</ui:CardControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:UiPage>
|
</ui:UiPage>
|
||||||
|
@ -16,7 +16,7 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
|
|||||||
|
|
||||||
// fullscreen optimizations were only added in windows 10 build 17093
|
// fullscreen optimizations were only added in windows 10 build 17093
|
||||||
if (Environment.OSVersion.Version.Build < 17093)
|
if (Environment.OSVersion.Version.Build < 17093)
|
||||||
this.MiscellaneousOptions.Visibility = Visibility.Collapsed;
|
this.FullscreenOptimizationsToggle.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ namespace Bloxstrap.UI
|
|||||||
string Message { get; set; }
|
string Message { get; set; }
|
||||||
ProgressBarStyle ProgressStyle { get; set; }
|
ProgressBarStyle ProgressStyle { get; set; }
|
||||||
int ProgressValue { get; set; }
|
int ProgressValue { get; set; }
|
||||||
|
int ProgressMaximum { get; set; }
|
||||||
bool CancelEnabled { get; set; }
|
bool CancelEnabled { get; set; }
|
||||||
|
|
||||||
void ShowBootstrapper();
|
void ShowBootstrapper();
|
||||||
|
@ -16,6 +16,7 @@ namespace Bloxstrap.UI
|
|||||||
|
|
||||||
private ActivityWatcher? _activityWatcher;
|
private ActivityWatcher? _activityWatcher;
|
||||||
private DiscordRichPresence? _richPresenceHandler;
|
private DiscordRichPresence? _richPresenceHandler;
|
||||||
|
private int? _processId;
|
||||||
|
|
||||||
EventHandler? _alertClickHandler;
|
EventHandler? _alertClickHandler;
|
||||||
|
|
||||||
@ -52,6 +53,14 @@ namespace Bloxstrap.UI
|
|||||||
if (App.Settings.Prop.ShowServerDetails)
|
if (App.Settings.Prop.ShowServerDetails)
|
||||||
_activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin);
|
_activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetProcessId(int processId)
|
||||||
|
{
|
||||||
|
if (_processId is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_processId = processId;
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Context menu
|
#region Context menu
|
||||||
@ -62,7 +71,7 @@ namespace Bloxstrap.UI
|
|||||||
|
|
||||||
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
|
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
|
||||||
|
|
||||||
_menuContainer = new(_activityWatcher, _richPresenceHandler);
|
_menuContainer = new(_activityWatcher, _richPresenceHandler, _processId);
|
||||||
_menuContainer.ShowDialog();
|
_menuContainer.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
|
|||||||
public ImageSource Icon { get; set; } = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
|
public ImageSource Icon { get; set; } = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
|
||||||
public string Message { get; set; } = "Please wait...";
|
public string Message { get; set; } = "Please wait...";
|
||||||
public bool ProgressIndeterminate { get; set; } = true;
|
public bool ProgressIndeterminate { get; set; } = true;
|
||||||
|
public int ProgressMaximum { get; set; } = 0;
|
||||||
public int ProgressValue { get; set; } = 0;
|
public int ProgressValue { get; set; } = 0;
|
||||||
|
|
||||||
public bool CancelEnabled { get; set; } = false;
|
public bool CancelEnabled { get; set; } = false;
|
||||||
|
@ -16,26 +16,11 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
|
|||||||
|
|
||||||
public Visibility VersionTextVisibility => CancelEnabled ? Visibility.Collapsed : Visibility.Visible;
|
public Visibility VersionTextVisibility => CancelEnabled ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
public string VersionText
|
public string VersionText { get; init; }
|
||||||
{
|
|
||||||
get
|
public ByfronDialogViewModel(IBootstrapperDialog dialog, string version) : base(dialog)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
VersionText = version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
{
|
{
|
||||||
public class BehaviourViewModel : NotifyPropertyChangedViewModel
|
public class BehaviourViewModel : NotifyPropertyChangedViewModel
|
||||||
{
|
{
|
||||||
private string _oldVersionGuid = "";
|
private string _oldPlayerVersionGuid = "";
|
||||||
|
private string _oldStudioVersionGuid = "";
|
||||||
|
|
||||||
public BehaviourViewModel()
|
public BehaviourViewModel()
|
||||||
{
|
{
|
||||||
@ -108,17 +109,22 @@
|
|||||||
|
|
||||||
public bool ForceRobloxReinstallation
|
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
|
set
|
||||||
{
|
{
|
||||||
if (value)
|
if (value)
|
||||||
{
|
{
|
||||||
_oldVersionGuid = App.State.Prop.VersionGuid;
|
_oldPlayerVersionGuid = App.State.Prop.PlayerVersionGuid;
|
||||||
App.State.Prop.VersionGuid = "";
|
_oldStudioVersionGuid = App.State.Prop.StudioVersionGuid;
|
||||||
|
App.State.Prop.PlayerVersionGuid = "";
|
||||||
|
App.State.Prop.StudioVersionGuid = "";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
App.State.Prop.VersionGuid = _oldVersionGuid;
|
App.State.Prop.PlayerVersionGuid = _oldPlayerVersionGuid;
|
||||||
|
App.State.Prop.StudioVersionGuid = _oldStudioVersionGuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,10 +82,21 @@ namespace Bloxstrap.UI.ViewModels.Menu
|
|||||||
set => App.FastFlags.SetPreset("UI.Menu.GraphicsSlider", value ? "True" : null);
|
set => App.FastFlags.SetPreset("UI.Menu.GraphicsSlider", value ? "True" : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Pre2022TexturesEnabled
|
public IReadOnlyDictionary<string, string> MaterialVersions => FastFlagManager.MaterialVersions;
|
||||||
|
|
||||||
|
public string SelectedMaterialVersion
|
||||||
{
|
{
|
||||||
get => App.FastFlags.GetPreset("Rendering.TexturePack") == FastFlagManager.OldTexturesFlagValue;
|
get
|
||||||
set => App.FastFlags.SetPreset("Rendering.TexturePack", value ? FastFlagManager.OldTexturesFlagValue : null);
|
{
|
||||||
|
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<string, Dictionary<string, string?>> IGMenuVersions => FastFlagManager.IGMenuVersions;
|
public IReadOnlyDictionary<string, Dictionary<string, string?>> IGMenuVersions => FastFlagManager.IGMenuVersions;
|
||||||
|
@ -47,5 +47,23 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
return version1.CompareTo(version2);
|
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(", ", ".");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ namespace Bloxstrap.Utility
|
|||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(filePath);
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
|
||||||
if (!fileInfo.IsReadOnly)
|
if (!fileInfo.Exists || !fileInfo.IsReadOnly)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
fileInfo.IsReadOnly = false;
|
fileInfo.IsReadOnly = false;
|
||||||
|
2
wpfui
2
wpfui
@ -1 +1 @@
|
|||||||
Subproject commit 55d5ca08f9a1d7623f9a7e386e1f4ac9f2d024a7
|
Subproject commit 2a50f387e6c3b0a9160f3ce42bc95fbb7185e87d
|
Loading…
Reference in New Issue
Block a user