diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 12b62fa..557d0c8 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Web; using System.Windows; using System.Windows.Threading; @@ -14,7 +15,8 @@ namespace Bloxstrap { public const string ProjectName = "Bloxstrap"; public const string ProjectRepository = "pizzaboxer/bloxstrap"; - public const string RobloxAppName = "RobloxPlayerBeta"; + public const string RobloxPlayerAppName = "RobloxPlayerBeta"; + public const string RobloxStudioAppName = "RobloxStudioBeta"; // used only for communicating between app and menu - use Directories.Base for anything else public static string BaseDirectory = null!; @@ -49,7 +51,9 @@ namespace Bloxstrap ) ); +#if RELEASE private static bool _showingExceptionDialog = false; +#endif public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) { @@ -120,6 +124,10 @@ namespace Bloxstrap LaunchArgs = e.Args; +#if DEBUG + Logger.WriteLine(LOG_IDENT, $"Arguments: {string.Join(' ', LaunchArgs)}"); +#endif + HttpClient.Timeout = TimeSpan.FromSeconds(30); HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); @@ -189,6 +197,7 @@ namespace Bloxstrap #endif string commandLine = ""; + LaunchMode? launchMode = null; if (IsMenuLaunch) { @@ -216,6 +225,8 @@ namespace Bloxstrap if (LaunchArgs[0].StartsWith("roblox-player:")) { commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); + + launchMode = LaunchMode.Player; } else if (LaunchArgs[0].StartsWith("roblox:")) { @@ -226,25 +237,53 @@ namespace Bloxstrap ); 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 { commandLine = "--app"; + + launchMode = LaunchMode.Player; } } else { commandLine = "--app"; + + launchMode = LaunchMode.Player; } - if (!String.IsNullOrEmpty(commandLine)) + if (launchMode != null) { if (!IsFirstRun) ShouldSaveConfigs = true; // start bootstrapper and show the bootstrapper modal if we're not running silently Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); - Bootstrapper bootstrapper = new(commandLine); + Bootstrapper bootstrapper = new(commandLine, (LaunchMode)launchMode); IBootstrapperDialog? dialog = null; if (!IsQuiet) @@ -261,7 +300,7 @@ namespace Bloxstrap Mutex? singletonMutex = null; - if (Settings.Prop.MultiInstanceLaunching) + if (Settings.Prop.MultiInstanceLaunching && launchMode == LaunchMode.Player) { Logger.WriteLine(LOG_IDENT, "Creating singleton mutex"); diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 8ce1388..36a6796 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -45,6 +45,7 @@ all + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index d68c50c..c4205f7 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -10,38 +10,8 @@ namespace Bloxstrap public class Bootstrapper { #region Properties - // in case a new package is added, you can find the corresponding directory - // by opening the stock bootstrapper in a hex editor - // TODO - there ideally should be a less static way to do this that's not hardcoded? - private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() - { - { "RobloxApp.zip", @"" }, - { "shaders.zip", @"shaders\" }, - { "ssl.zip", @"ssl\" }, - - // the runtime installer is only extracted if it needs installing - { "WebView2.zip", @"" }, - { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, - - { "content-avatar.zip", @"content\avatar\" }, - { "content-configs.zip", @"content\configs\" }, - { "content-fonts.zip", @"content\fonts\" }, - { "content-sky.zip", @"content\sky\" }, - { "content-sounds.zip", @"content\sounds\" }, - { "content-textures2.zip", @"content\textures\" }, - { "content-models.zip", @"content\models\" }, - - { "content-textures3.zip", @"PlatformContent\pc\textures\" }, - { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, - { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, - - { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, - { "extracontent-translations.zip", @"ExtraContent\translations\" }, - { "extracontent-models.zip", @"ExtraContent\models\" }, - { "extracontent-textures.zip", @"ExtraContent\textures\" }, - { "extracontent-places.zip", @"ExtraContent\places\" }, - }; - + private const int ProgressBarMaximum = 10000; + private const string AppSettings = "\r\n" + "\r\n" + @@ -51,15 +21,49 @@ namespace Bloxstrap 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 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 PackageManifest _versionPackageManifest = null!; - private FileManifest _versionFileManifest = null!; private string _versionFolder = null!; private bool _isInstalling = false; @@ -68,19 +72,33 @@ namespace Bloxstrap private int _packagesExtracted = 0; private bool _cancelFired = false; + private IReadOnlyDictionary _packageDirectories; + public IBootstrapperDialog? Dialog = null; + + public bool IsStudioLaunch => _launchMode != LaunchMode.Player; #endregion #region Core - public Bootstrapper(string launchCommandLine) + public Bootstrapper(string launchCommandLine, LaunchMode launchMode) { _launchCommandLine = launchCommandLine; + _launchMode = launchMode; + + _packageDirectories = _launchMode == LaunchMode.Player ? PackageMap.Player : PackageMap.Studio; } private void SetStatus(string message) { App.Logger.WriteLine("Bootstrapper::SetStatus", message); + string productName = "Roblox"; + + if (_launchMode != LaunchMode.Player) + productName += " Studio"; + + message = message.Replace("{product}", productName); + // yea idk if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) message = message.Replace("...", ""); @@ -91,15 +109,16 @@ namespace Bloxstrap 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 // too lazy to fix properly so lol - if (newProgress > 100) - return; + progressValue = Math.Clamp(progressValue, 0, ProgressBarMaximum); - if (Dialog is not null) - Dialog.ProgressValue = newProgress; + Dialog.ProgressValue = progressValue; } public async Task Run() @@ -183,7 +202,7 @@ namespace Bloxstrap await CheckLatestVersion(); // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (App.IsFirstRun || _latestVersionGuid != App.State.Prop.VersionGuid || !File.Exists(_playerLocation)) + if (App.IsFirstRun || _latestVersionGuid != _versionGuid || !File.Exists(_playerLocation)) await InstallLatestVersion(); if (App.IsFirstRun) @@ -223,18 +242,20 @@ namespace Bloxstrap ClientVersion clientVersion; + string binaryType = _launchMode == LaunchMode.Player ? "WindowsPlayer" : "WindowsStudio64"; + try { - clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType); } catch (HttpResponseException ex) { if (ex.ResponseMessage.StatusCode != HttpStatusCode.NotFound) throw; - App.Logger.WriteLine(LOG_IDENT, $"Reverting enrolled channel to {RobloxDeployment.DefaultChannel} because a WindowsPlayer build does not exist for {App.Settings.Prop.Channel}"); + App.Logger.WriteLine(LOG_IDENT, $"Reverting enrolled channel to {RobloxDeployment.DefaultChannel} because a {binaryType} build does not exist for {App.Settings.Prop.Channel}"); App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; - clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType); } if (clientVersion.IsBehindDefaultChannel) @@ -257,21 +278,20 @@ namespace Bloxstrap App.Logger.WriteLine("Bootstrapper::CheckLatestVersion", $"Changed Roblox channel from {App.Settings.Prop.Channel} to {RobloxDeployment.DefaultChannel}"); App.Settings.Prop.Channel = RobloxDeployment.DefaultChannel; - clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel); + clientVersion = await RobloxDeployment.GetInfo(App.Settings.Prop.Channel, binaryType: binaryType); } } _latestVersionGuid = clientVersion.VersionGuid; _versionFolder = Path.Combine(Paths.Versions, _latestVersionGuid); _versionPackageManifest = await PackageManifest.Get(_latestVersionGuid); - _versionFileManifest = await FileManifest.Get(_latestVersionGuid); } private async Task StartRoblox() { const string LOG_IDENT = "Bootstrapper::StartRoblox"; - SetStatus("Starting Roblox..."); + SetStatus("Starting {product}..."); if (_launchCommandLine == "--app" && App.Settings.Prop.UseDisableAppPatch) { @@ -294,21 +314,38 @@ namespace Bloxstrap 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()) - _launchCommandLine += "production"; - else - _launchCommandLine += App.Settings.Prop.Channel.ToLowerInvariant(); + if (App.Settings.Prop.Channel.ToLowerInvariant() == RobloxDeployment.DefaultChannel.ToLowerInvariant()) + _launchCommandLine += "production"; + else + _launchCommandLine += App.Settings.Prop.Channel.ToLowerInvariant(); + } // whether we should wait for roblox to exit to handle stuff in the background or clean up after roblox closes bool shouldWait = false; + 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 int gameClientPid; - using (Process gameClient = Process.Start(_playerLocation, _launchCommandLine)) + using (Process gameClient = Process.Start(startInfo)!) { gameClientPid = gameClient.Id; } @@ -319,7 +356,8 @@ namespace Bloxstrap 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(); @@ -329,6 +367,9 @@ namespace Bloxstrap return; } + if (App.Settings.Prop.EnableActivityTracking && _launchMode == LaunchMode.Player) + App.NotifyIcon?.SetProcessId(gameClientPid); + if (App.Settings.Prop.EnableActivityTracking) { activityWatcher = new(); @@ -476,7 +517,10 @@ namespace Bloxstrap using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{App.ProjectName}"); // sum compressed and uncompressed package sizes and convert to kilobytes - int totalSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; + int distributionSize = (_versionPackageManifest.Sum(x => x.Size) + _versionPackageManifest.Sum(x => x.PackedSize)) / 1000; + _distributionSize = distributionSize; + + int totalSize = App.State.Prop.PlayerSize + App.State.Prop.StudioSize; uninstallKey.SetValue("EstimatedSize", totalSize); @@ -495,14 +539,26 @@ namespace Bloxstrap ProtocolHandler.Register("roblox", "Roblox", Paths.Application); ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application); + ProtocolHandler.Register("roblox-studio", "Roblox", Paths.Application); + ProtocolHandler.Register("roblox-studio-auth", "Roblox", Paths.Application); - // in case the user is reinstalling - if (File.Exists(Paths.Application) && App.IsFirstRun) - File.Delete(Paths.Application); + ProtocolHandler.RegisterRobloxPlace(Paths.Application); + ProtocolHandler.RegisterExtension(".rbxl"); + ProtocolHandler.RegisterExtension(".rbxlx"); - // check to make sure bootstrapper is in the install folder - if (!File.Exists(Paths.Application) && Environment.ProcessPath is not null) - File.Copy(Environment.ProcessPath, Paths.Application); + if (Environment.ProcessPath is not null && 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(), // 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, "-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) { @@ -630,7 +687,7 @@ namespace Bloxstrap const string LOG_IDENT = "Bootstrapper::Uninstall"; // prompt to shutdown roblox if its currently running - if (Process.GetProcessesByName(App.RobloxAppName).Any()) + if (Process.GetProcessesByName(App.RobloxPlayerAppName).Any() || Process.GetProcessesByName(App.RobloxStudioAppName).Any()) { App.Logger.WriteLine(LOG_IDENT, $"Prompting to shut down all open Roblox instances"); @@ -645,7 +702,13 @@ namespace Bloxstrap try { - foreach (Process process in Process.GetProcessesByName("RobloxPlayerBeta")) + foreach (Process process in Process.GetProcessesByName(App.RobloxPlayerAppName)) + { + process.CloseMainWindow(); + process.Close(); + } + + foreach (Process process in Process.GetProcessesByName(App.RobloxStudioAppName)) { process.CloseMainWindow(); process.Close(); @@ -662,16 +725,17 @@ namespace Bloxstrap SetStatus($"Uninstalling {App.ProjectName}..."); App.ShouldSaveConfigs = false; - bool robloxStillInstalled = true; + bool robloxPlayerStillInstalled = true; + bool robloxStudioStillInstalled = true; // check if stock bootstrapper is still installed - RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + using RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); if (bootstrapperKey is null) { + robloxPlayerStillInstalled = false; + ProtocolHandler.Unregister("roblox"); ProtocolHandler.Unregister("roblox-player"); - - robloxStillInstalled = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio") is not null; } else { @@ -683,6 +747,27 @@ namespace Bloxstrap ProtocolHandler.Register("roblox-player", "Roblox", bootstrapperLocation); } + using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); + if (studioBootstrapperKey is null) + { + robloxStudioStillInstalled = false; + + ProtocolHandler.Unregister("roblox-studio"); + ProtocolHandler.Unregister("roblox-studio-auth"); + + ProtocolHandler.Unregister("Roblox.Place"); + ProtocolHandler.Unregister(".rbxl"); + ProtocolHandler.Unregister(".rbxlx"); + } + else + { + string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper + ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation); + ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation); + + ProtocolHandler.RegisterRobloxPlace(studioLocation); + } + // if the folder we're installed to does not end with "Bloxstrap", we're installed to a user-selected folder // in which case, chances are they chose to install to somewhere they didn't really mean to (prior to the added warning in 2.4.0) // if so, we're walking on eggshells and have to ensure we only clean up what we need to clean up @@ -713,7 +798,7 @@ namespace Bloxstrap string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox"); - if (!robloxStillInstalled && Directory.Exists(robloxFolder)) + if (!robloxPlayerStillInstalled && !robloxStudioStillInstalled && Directory.Exists(robloxFolder)) cleanupSequence.Add(() => Directory.Delete(robloxFolder, true)); foreach (var process in cleanupSequence) @@ -767,7 +852,7 @@ namespace Bloxstrap _isInstalling = true; - SetStatus(FreshInstall ? "Installing Roblox..." : "Upgrading Roblox..."); + SetStatus(FreshInstall ? "Installing {product}..." : "Upgrading {product}..."); Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Downloads); @@ -793,10 +878,12 @@ namespace Bloxstrap { Dialog.CancelEnabled = true; Dialog.ProgressStyle = ProgressBarStyle.Continuous; - } - // compute total bytes to download - _progressIncrement = (double)100 / _versionPackageManifest.Sum(package => package.PackedSize); + Dialog.ProgressMaximum = ProgressBarMaximum; + + // compute total bytes to download + _progressIncrement = (double)ProgressBarMaximum / _versionPackageManifest.Sum(package => package.PackedSize); + } foreach (Package package in _versionPackageManifest) { @@ -812,7 +899,7 @@ namespace Bloxstrap // extract the package immediately after download asynchronously // discard is just used to suppress the warning - _ = ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}"); + _ = Task.Run(() => ExtractPackage(package).ContinueWith(AsyncHelpers.ExceptionHandler, $"extracting {package.Name}")); } if (_cancelFired) @@ -824,7 +911,7 @@ namespace Bloxstrap if (Dialog is not null) { Dialog.ProgressStyle = ProgressBarStyle.Marquee; - SetStatus("Configuring Roblox..."); + SetStatus("Configuring {product}..."); } // wait for all packages to finish extracting, with an exception for the webview2 runtime installer @@ -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 using (RegistryKey appFlagsKey = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers")) { - string oldGameClientLocation = Path.Combine(oldVersionFolder, "RobloxPlayerBeta.exe"); + string oldGameClientLocation = Path.Combine(oldVersionFolder, _playerFileName); string? appFlags = (string?)appFlagsKey.GetValue(oldGameClientLocation); if (appFlags is not null) @@ -876,34 +963,34 @@ namespace Bloxstrap appFlagsKey.DeleteValue(oldGameClientLocation); } } + } - // delete any old version folders - // we only do this if roblox isnt running just in case an update happened - // while they were launching a second instance or something idk - if (!Process.GetProcessesByName(App.RobloxAppName).Any()) + _versionGuid = _latestVersionGuid; + + // delete any old version folders + // we only do this if roblox isnt running just in case an update happened + // while they were launching a second instance or something idk + if (!Process.GetProcessesByName(App.RobloxPlayerAppName).Any() && !Process.GetProcessesByName(App.RobloxStudioAppName).Any()) + { + foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories()) { - foreach (DirectoryInfo dir in new DirectoryInfo(Paths.Versions).GetDirectories()) + if (dir.Name == 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-")) - continue; - - App.Logger.WriteLine(LOG_IDENT, $"Removing old version folder for {dir.Name}"); - - try - { - dir.Delete(true); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!"); - App.Logger.WriteException(LOG_IDENT, ex); - } + dir.Delete(true); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to delete version folder!"); + App.Logger.WriteException(LOG_IDENT, ex); } } } - App.State.Prop.VersionGuid = _latestVersionGuid; - // don't register program size until the program is registered, which will be done after this if (!App.IsFirstRun && !FreshInstall) RegisterProgramSize(); @@ -990,7 +1077,7 @@ namespace Bloxstrap { const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - if (Process.GetProcessesByName("RobloxPlayerBeta").Any()) + if (Process.GetProcessesByName(_playerFileName[..^4]).Any()) { App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); return; @@ -1182,6 +1269,7 @@ namespace Bloxstrap Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); + Filesystem.AssertReadOnly(fileVersionFolder); File.Copy(fileModFolder, fileVersionFolder, true); Filesystem.AssertReadOnly(fileVersionFolder); @@ -1196,7 +1284,7 @@ namespace Bloxstrap if (modFolderFiles.Contains(fileLocation)) continue; - var package = PackageDirectories.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + var package = _packageDirectories.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); // package doesn't exist, likely mistakenly placed file if (String.IsNullOrEmpty(package.Key)) @@ -1361,9 +1449,11 @@ namespace Bloxstrap _totalDownloadedBytes += bytesRead; UpdateProgressBar(); } - - if (MD5Hash.FromStream(fileStream) != package.Signature) - throw new Exception("Signature does not match!"); + + string hash = MD5Hash.FromStream(fileStream); + + 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)"); break; @@ -1373,12 +1463,12 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})"); App.Logger.WriteException(LOG_IDENT, ex); + if (i >= maxTries || ex.GetType() == typeof(ChecksumFailedException)) + throw; + if (File.Exists(packageLocation)) File.Delete(packageLocation); - if (i >= maxTries) - throw; - _totalDownloadedBytes -= totalBytesRead; UpdateProgressBar(); @@ -1394,75 +1484,26 @@ namespace Bloxstrap } } - private async Task ExtractPackage(Package package) + private Task ExtractPackage(Package package) { const string LOG_IDENT = "Bootstrapper::ExtractPackage"; if (_cancelFired) - return; + return Task.CompletedTask; string packageLocation = Path.Combine(Paths.Downloads, package.Signature); - string packageFolder = Path.Combine(_versionFolder, PackageDirectories[package.Name]); + 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)); - - 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); - } + var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip(); + fastZip.ExtractZip(packageLocation, packageFolder, null); App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); _packagesExtracted += 1; + + return Task.CompletedTask; } private async Task ExtractFileFromPackage(string packageName, string fileName) @@ -1481,7 +1522,7 @@ namespace Bloxstrap if (entry is null) return; - string extractionPath = Path.Combine(_versionFolder, PackageDirectories[package.Name], entry.FullName); + string extractionPath = Path.Combine(_versionFolder, _packageDirectories[package.Name], entry.FullName); entry.ExtractToFile(extractionPath, true); } #endregion diff --git a/Bloxstrap/Enums/LaunchMode.cs b/Bloxstrap/Enums/LaunchMode.cs new file mode 100644 index 0000000..d1a34e9 --- /dev/null +++ b/Bloxstrap/Enums/LaunchMode.cs @@ -0,0 +1,9 @@ +namespace Bloxstrap.Enums +{ + public enum LaunchMode + { + Player, + Studio, + StudioAuth + } +} diff --git a/Bloxstrap/Exceptions/ChecksumFailedException.cs b/Bloxstrap/Exceptions/ChecksumFailedException.cs new file mode 100644 index 0000000..95d8af2 --- /dev/null +++ b/Bloxstrap/Exceptions/ChecksumFailedException.cs @@ -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) + { + } + } +} diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index eb66599..b574732 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -11,6 +11,7 @@ namespace Bloxstrap // 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 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 PresetFlags = new Dictionary { @@ -25,9 +26,11 @@ namespace Bloxstrap { "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" }, { "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" }, - { "Rendering.TexturePack", "FStringPartTexturePackTable2022" }, { "Rendering.DisableScaling", "DFFlagDisableDPIScale" }, + { "Rendering.Materials.NewTexturePack", "FStringPartTexturePackTable2022" }, + { "Rendering.Materials.OldTexturePack", "FStringPartTexturePackTablePre2022" }, + { "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" }, { "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" }, { "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" }, @@ -80,6 +83,13 @@ namespace Bloxstrap { "8x MSAA", "8" } }; + public static IReadOnlyDictionary MaterialVersions => new Dictionary + { + { "Chosen by game", "None" }, + { "Old (Pre-2022)", "NewTexturePack" }, + { "New (2022)", "OldTexturePack" } + }; + // 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? public static IReadOnlyDictionary> IGMenuVersions => new Dictionary> diff --git a/Bloxstrap/Models/State.cs b/Bloxstrap/Models/State.cs index 6f1d650..5c22be6 100644 --- a/Bloxstrap/Models/State.cs +++ b/Bloxstrap/Models/State.cs @@ -3,7 +3,15 @@ public class State { public string LastEnrolledChannel { get; set; } = ""; - public string VersionGuid { get; set; } = ""; + + [Obsolete("Use PlayerVersionGuid instead", true)] + public string VersionGuid { set { PlayerVersionGuid = value; } } + public string PlayerVersionGuid { get; set; } = ""; + public string StudioVersionGuid { get; set; } = ""; + + public int PlayerSize { get; set; } = 0; + public int StudioSize { get; set; } = 0; + public List ModManifest { get; set; } = new(); } } diff --git a/Bloxstrap/PackageMap.cs b/Bloxstrap/PackageMap.cs new file mode 100644 index 0000000..6932921 --- /dev/null +++ b/Bloxstrap/PackageMap.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap +{ + internal class PackageMap + { + public static IReadOnlyDictionary Player + { + get { return CombineDictionaries(_common, _playerOnly); } + } + + public static IReadOnlyDictionary Studio + { + get { return CombineDictionaries(_common, _studioOnly); } + } + + // in case a new package is added, you can find the corresponding directory + // by opening the stock bootstrapper in a hex editor + // TODO - there ideally should be a less static way to do this that's not hardcoded? + private static IReadOnlyDictionary _common = new Dictionary() + { + { "Libraries.zip", @"" }, + { "shaders.zip", @"shaders\" }, + { "ssl.zip", @"ssl\" }, + + // the runtime installer is only extracted if it needs installing + { "WebView2.zip", @"" }, + { "WebView2RuntimeInstaller.zip", @"WebView2RuntimeInstaller\" }, + + { "content-avatar.zip", @"content\avatar\" }, + { "content-configs.zip", @"content\configs\" }, + { "content-fonts.zip", @"content\fonts\" }, + { "content-sky.zip", @"content\sky\" }, + { "content-sounds.zip", @"content\sounds\" }, + { "content-textures2.zip", @"content\textures\" }, + { "content-models.zip", @"content\models\" }, + + { "content-textures3.zip", @"PlatformContent\pc\textures\" }, + { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, + { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, + + { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, + { "extracontent-translations.zip", @"ExtraContent\translations\" }, + { "extracontent-models.zip", @"ExtraContent\models\" }, + { "extracontent-textures.zip", @"ExtraContent\textures\" }, + { "extracontent-places.zip", @"ExtraContent\places\" }, + }; + + private static IReadOnlyDictionary _playerOnly = new Dictionary() + { + { "RobloxApp.zip", @"" } + }; + + private static IReadOnlyDictionary _studioOnly = new Dictionary() + { + { "RobloxStudio.zip", @"" }, + { "ApplicationConfig.zip", @"ApplicationConfig\" }, + { "content-studio_svg_textures.zip", @"content\studio_svg_textures\"}, + { "content-qt_translations.zip", @"content\qt_translations\" }, + { "content-api-docs.zip", @"content\api_docs\" }, + { "extracontent-scripts.zip", @"ExtraContent\scripts\" }, + { "BuiltInPlugins.zip", @"BuiltInPlugins\" }, + { "BuiltInStandalonePlugins.zip", @"BuiltInStandalonePlugins\" }, + { "LibrariesQt5.zip", @"" }, + { "Plugins.zip", @"Plugins\" }, + { "Qml.zip", @"Qml\" }, + { "StudioFonts.zip", @"StudioFonts\" }, + { "redist.zip", @"" }, + }; + + private static Dictionary CombineDictionaries(IReadOnlyDictionary d1, IReadOnlyDictionary d2) + { + Dictionary newD = new Dictionary(); + + foreach (var d in d1) + newD[d.Key] = d.Value; + + foreach (var d in d2) + newD[d.Key] = d.Value; + + return newD; + } + } +} diff --git a/Bloxstrap/Properties/launchSettings.json b/Bloxstrap/Properties/launchSettings.json index ee3606b..2cf74a5 100644 --- a/Bloxstrap/Properties/launchSettings.json +++ b/Bloxstrap/Properties/launchSettings.json @@ -22,6 +22,10 @@ "Bloxstrap (Deeplink)": { "commandName": "Project", "commandLineArgs": "roblox://experiences/start?placeId=95206881" + }, + "Bloxstrap (Studio Launch)": { + "commandName": "Project", + "commandLineArgs": "-ide" } } } \ No newline at end of file diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/ProtocolHandler.cs index 44ff873..3193d63 100644 --- a/Bloxstrap/ProtocolHandler.cs +++ b/Bloxstrap/ProtocolHandler.cs @@ -7,6 +7,8 @@ namespace Bloxstrap { static class ProtocolHandler { + private const string RobloxPlaceKey = "Roblox.Place"; + // map uri keys to command line args private static readonly IReadOnlyDictionary UriKeyArgMap = new Dictionary() { @@ -18,7 +20,12 @@ namespace Bloxstrap { "browsertrackerid", "-b " }, { "robloxLocale", "--rloc " }, { "gameLocale", "--gloc " }, - { "channel", "-channel " } + { "channel", "-channel " }, + // studio + { "task", "-task " }, + { "placeId", "-placeId " }, + { "universeId", "-universeId " }, + { "userId", "-userId " } }; public static string ParseUri(string protocol) @@ -108,9 +115,10 @@ namespace Bloxstrap public static void Register(string key, string name, string handler) { string handlerArgs = $"\"{handler}\" %1"; - RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); - RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon"); - RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command"); + + using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); + using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon"); + using RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command"); if (uriKey.GetValue("") is null) { @@ -118,15 +126,44 @@ namespace Bloxstrap uriKey.SetValue("URL Protocol", ""); } - if ((string?)uriCommandKey.GetValue("") != handlerArgs) + if (uriCommandKey.GetValue("") as string != handlerArgs) { uriIconKey.SetValue("", handler); uriCommandKey.SetValue("", handlerArgs); } + } - uriKey.Close(); - uriIconKey.Close(); - uriCommandKey.Close(); + public static void RegisterRobloxPlace(string handler) + { + const string keyValue = "Roblox Place"; + string handlerArgs = $"\"{handler}\" -ide \"%1\""; + string iconValue = $"{handler},0"; + + using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey(@"Software\Classes\" + RobloxPlaceKey); + using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon"); + using RegistryKey uriOpenKey = uriKey.CreateSubKey(@"shell\Open"); + using RegistryKey uriCommandKey = uriOpenKey.CreateSubKey(@"command"); + + if (uriKey.GetValue("") as string != keyValue) + uriKey.SetValue("", keyValue); + + if (uriCommandKey.GetValue("") as string != handlerArgs) + uriCommandKey.SetValue("", handlerArgs); + + if (uriOpenKey.GetValue("") as string != "Open") + uriOpenKey.SetValue("", "Open"); + + if (uriIconKey.GetValue("") as string != iconValue) + uriIconKey.SetValue("", iconValue); + } + + public static void RegisterExtension(string key) + { + using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); + uriKey.CreateSubKey(RobloxPlaceKey + @"\ShellNew"); + + if (uriKey.GetValue("") as string != RobloxPlaceKey) + uriKey.SetValue("", RobloxPlaceKey); } public static void Unregister(string key) diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index 5ce4b19..2a3a606 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -74,22 +74,23 @@ return location; } - public static async Task GetInfo(string channel, bool extraInformation = false) + public static async Task GetInfo(string channel, bool extraInformation = false, string binaryType = "WindowsPlayer") { const string LOG_IDENT = "RobloxDeployment::GetInfo"; App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel} (extraInformation={extraInformation})"); + string cacheKey = $"{channel}-{binaryType}"; ClientVersion clientVersion; - if (ClientVersionCache.ContainsKey(channel)) + if (ClientVersionCache.ContainsKey(cacheKey)) { App.Logger.WriteLine(LOG_IDENT, "Deploy information is cached"); - clientVersion = ClientVersionCache[channel]; + clientVersion = ClientVersionCache[cacheKey]; } else { - string path = $"/v2/client-version/WindowsPlayer/channel/{channel}"; + string path = $"/v2/client-version/{binaryType}/channel/{channel}"; HttpResponseMessage deployInfoResponse; try @@ -152,7 +153,7 @@ } } - ClientVersionCache[channel] = clientVersion; + ClientVersionCache[cacheKey] = clientVersion; return clientVersion; } diff --git a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs index 69d4b36..d59bdc5 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/Base/WinFormsDialogBase.cs @@ -14,6 +14,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base protected virtual string _message { get; set; } = "Please wait..."; protected virtual ProgressBarStyle _progressStyle { get; set; } protected virtual int _progressValue { get; set; } + protected virtual int _progressMaximum { get; set; } protected virtual bool _cancelEnabled { get; set; } 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 { get => _progressValue; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml index 183be50..1a0b6c8 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml @@ -38,7 +38,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs index b01d083..4d709ec 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ByfronDialog.xaml.cs @@ -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 { get => _viewModel.ProgressValue; @@ -69,7 +79,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper public ByfronDialog() { - _viewModel = new ByfronDialogViewModel(this); + string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false); + _viewModel = new ByfronDialogViewModel(this, version); DataContext = _viewModel; Title = App.Settings.Prop.BootstrapperTitle; Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource(); diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml index 40cc7f9..ce265a6 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml +++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml @@ -36,7 +36,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs index 7ab6cdc..780e75d 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/FluentDialog.xaml.cs @@ -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 { get => _viewModel.ProgressValue; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs index ce2a1e8..c1dd219 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2008.cs @@ -21,6 +21,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper set => ProgressBar.Style = value; } + protected override int _progressMaximum + { + get => ProgressBar.Maximum; + set => ProgressBar.Maximum = value; + } + protected override int _progressValue { get => ProgressBar.Value; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs index 5f92d4e..7248b1c 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/LegacyDialog2011.cs @@ -20,6 +20,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper set => ProgressBar.Style = value; } + protected override int _progressMaximum + { + get => ProgressBar.Maximum; + set => ProgressBar.Maximum = value; + } + protected override int _progressValue { get => ProgressBar.Value; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs b/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs index 4f9bb01..b00e8c6 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/ProgressDialog.cs @@ -21,6 +21,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper set => ProgressBar.Style = value; } + protected override int _progressMaximum + { + get => ProgressBar.Maximum; + set => ProgressBar.Maximum = value; + } + protected override int _progressValue { get => ProgressBar.Value; diff --git a/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs b/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs index 8a47f9c..c228bf8 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/VistaDialog.cs @@ -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 { get => _dialogPage.ProgressBar?.Value ?? 0; diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml index 58d48b3..95bb446 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml @@ -60,6 +60,18 @@ + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs index f2b8df0..4c4d192 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs @@ -26,14 +26,16 @@ namespace Bloxstrap.UI.Elements.ContextMenu private LogTracer? _logTracerWindow; private ServerInformation? _serverInformationWindow; + private int? _processId; - public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler) + public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler, int? processId) { InitializeComponent(); ApplyTheme(); _activityWatcher = activityWatcher; _richPresenceHandler = richPresenceHandler; + _processId = processId; if (_activityWatcher is not null) { @@ -47,6 +49,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu if (_richPresenceHandler is not null) RichPresenceMenuItem.Visibility = Visibility.Visible; + if (_processId is not null) + CloseRobloxMenuItem.Visibility = Visibility.Visible; + VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}"; } @@ -118,5 +123,21 @@ namespace Bloxstrap.UI.Elements.ContextMenu _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(); + } } } diff --git a/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml index df04fad..ce75096 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/AboutPage.xaml @@ -221,9 +221,15 @@ - + - + + + + + + + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml index 666e665..66bb6c4 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/FastFlagsPage.xaml @@ -150,17 +150,17 @@ - + - + - + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml index 51727ff..e9608ba 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml +++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml @@ -121,33 +121,31 @@ - - - - - - - - Forces every in-game font to be a font that you choose. - - - + + + - - + + + Forces every in-game font to be a font that you choose. + - - - - - - - A Windows feature that intends to improve fullscreen performance. See here for more information. - - - - - - + + + + + + + + + + + + A Windows feature that intends to improve fullscreen performance. See here for more information. + + + + + diff --git a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs index d46903e..8ae448b 100644 --- a/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Menu/Pages/ModsPage.xaml.cs @@ -16,7 +16,7 @@ namespace Bloxstrap.UI.Elements.Menu.Pages // fullscreen optimizations were only added in windows 10 build 17093 if (Environment.OSVersion.Version.Build < 17093) - this.MiscellaneousOptions.Visibility = Visibility.Collapsed; + this.FullscreenOptimizationsToggle.Visibility = Visibility.Collapsed; } } } diff --git a/Bloxstrap/UI/IBootstrapperDialog.cs b/Bloxstrap/UI/IBootstrapperDialog.cs index 4b44900..e15f7fb 100644 --- a/Bloxstrap/UI/IBootstrapperDialog.cs +++ b/Bloxstrap/UI/IBootstrapperDialog.cs @@ -9,6 +9,7 @@ namespace Bloxstrap.UI string Message { get; set; } ProgressBarStyle ProgressStyle { get; set; } int ProgressValue { get; set; } + int ProgressMaximum { get; set; } bool CancelEnabled { get; set; } void ShowBootstrapper(); diff --git a/Bloxstrap/UI/NotifyIconWrapper.cs b/Bloxstrap/UI/NotifyIconWrapper.cs index 6eb8e06..8ed8fea 100644 --- a/Bloxstrap/UI/NotifyIconWrapper.cs +++ b/Bloxstrap/UI/NotifyIconWrapper.cs @@ -16,6 +16,7 @@ namespace Bloxstrap.UI private ActivityWatcher? _activityWatcher; private DiscordRichPresence? _richPresenceHandler; + private int? _processId; EventHandler? _alertClickHandler; @@ -52,6 +53,14 @@ namespace Bloxstrap.UI if (App.Settings.Prop.ShowServerDetails) _activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin); } + + public void SetProcessId(int processId) + { + if (_processId is not null) + return; + + _processId = processId; + } #endregion #region Context menu @@ -62,7 +71,7 @@ namespace Bloxstrap.UI App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu"); - _menuContainer = new(_activityWatcher, _richPresenceHandler); + _menuContainer = new(_activityWatcher, _richPresenceHandler, _processId); _menuContainer.ShowDialog(); } diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs index 593921d..2a5c523 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/BootstrapperDialogViewModel.cs @@ -16,6 +16,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper public ImageSource Icon { get; set; } = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource(); public string Message { get; set; } = "Please wait..."; public bool ProgressIndeterminate { get; set; } = true; + public int ProgressMaximum { get; set; } = 0; public int ProgressValue { get; set; } = 0; public bool CancelEnabled { get; set; } = false; diff --git a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs index 5f16722..f652499 100644 --- a/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Bootstrapper/ByfronDialogViewModel.cs @@ -16,26 +16,11 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper public Visibility VersionTextVisibility => CancelEnabled ? Visibility.Collapsed : Visibility.Visible; - public string VersionText - { - get - { - string playerLocation = Path.Combine(Paths.Versions, App.State.Prop.VersionGuid, "RobloxPlayerBeta.exe"); - - if (!File.Exists(playerLocation)) - return ""; - - FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(playerLocation); - - if (versionInfo.ProductVersion is null) - return ""; - - return versionInfo.ProductVersion.Replace(", ", "."); - } - } - - public ByfronDialogViewModel(IBootstrapperDialog dialog) : base(dialog) + public string VersionText { get; init; } + + public ByfronDialogViewModel(IBootstrapperDialog dialog, string version) : base(dialog) { + VersionText = version; } } } diff --git a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs index 28c4bc7..de074b9 100644 --- a/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/BehaviourViewModel.cs @@ -2,7 +2,8 @@ { public class BehaviourViewModel : NotifyPropertyChangedViewModel { - private string _oldVersionGuid = ""; + private string _oldPlayerVersionGuid = ""; + private string _oldStudioVersionGuid = ""; public BehaviourViewModel() { @@ -108,17 +109,22 @@ public bool ForceRobloxReinstallation { - get => String.IsNullOrEmpty(App.State.Prop.VersionGuid); + // wouldnt it be better to check old version guids? + // what about fresh installs? + get => String.IsNullOrEmpty(App.State.Prop.PlayerVersionGuid) && String.IsNullOrEmpty(App.State.Prop.StudioVersionGuid); set { if (value) { - _oldVersionGuid = App.State.Prop.VersionGuid; - App.State.Prop.VersionGuid = ""; + _oldPlayerVersionGuid = App.State.Prop.PlayerVersionGuid; + _oldStudioVersionGuid = App.State.Prop.StudioVersionGuid; + App.State.Prop.PlayerVersionGuid = ""; + App.State.Prop.StudioVersionGuid = ""; } else { - App.State.Prop.VersionGuid = _oldVersionGuid; + App.State.Prop.PlayerVersionGuid = _oldPlayerVersionGuid; + App.State.Prop.StudioVersionGuid = _oldStudioVersionGuid; } } } diff --git a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs index 8dee2e5..8114c83 100644 --- a/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Menu/FastFlagsViewModel.cs @@ -82,10 +82,21 @@ namespace Bloxstrap.UI.ViewModels.Menu set => App.FastFlags.SetPreset("UI.Menu.GraphicsSlider", value ? "True" : null); } - public bool Pre2022TexturesEnabled + public IReadOnlyDictionary MaterialVersions => FastFlagManager.MaterialVersions; + + public string SelectedMaterialVersion { - get => App.FastFlags.GetPreset("Rendering.TexturePack") == FastFlagManager.OldTexturesFlagValue; - set => App.FastFlags.SetPreset("Rendering.TexturePack", value ? FastFlagManager.OldTexturesFlagValue : null); + get + { + 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> IGMenuVersions => FastFlagManager.IGMenuVersions; diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 565dc79..7f33a19 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -47,5 +47,23 @@ namespace Bloxstrap 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(", ", "."); + } } } diff --git a/Bloxstrap/Utility/Filesystem.cs b/Bloxstrap/Utility/Filesystem.cs index b9064fe..ff904ac 100644 --- a/Bloxstrap/Utility/Filesystem.cs +++ b/Bloxstrap/Utility/Filesystem.cs @@ -24,7 +24,7 @@ namespace Bloxstrap.Utility { var fileInfo = new FileInfo(filePath); - if (!fileInfo.IsReadOnly) + if (!fileInfo.Exists || !fileInfo.IsReadOnly) return; fileInfo.IsReadOnly = false; diff --git a/wpfui b/wpfui index 55d5ca0..2a50f38 160000 --- a/wpfui +++ b/wpfui @@ -1 +1 @@ -Subproject commit 55d5ca08f9a1d7623f9a7e386e1f4ac9f2d024a7 +Subproject commit 2a50f387e6c3b0a9160f3ce42bc95fbb7185e87d