diff --git a/Bloxstrap.sln b/Bloxstrap.sln new file mode 100644 index 00000000..62d830d7 --- /dev/null +++ b/Bloxstrap.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bloxstrap", "Bloxstrap\Bloxstrap.csproj", "{646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F989AC04-B48F-4BB4-B940-1E7D082F14DA} + EndGlobalSection +EndGlobal diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj new file mode 100644 index 00000000..cfe3a0c3 --- /dev/null +++ b/Bloxstrap/Bloxstrap.csproj @@ -0,0 +1,47 @@ + + + + WinExe + net6.0-windows + enable + true + enable + AnyCPU + AnyCPU;x86 + Bloxstrap.ico + 1.0.0 + 1.0.0.0 + + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap.ico b/Bloxstrap/Bloxstrap.ico new file mode 100644 index 00000000..6a50aff7 Binary files /dev/null and b/Bloxstrap/Bloxstrap.ico differ diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs new file mode 100644 index 00000000..a14dda02 --- /dev/null +++ b/Bloxstrap/Bootstrapper.cs @@ -0,0 +1,655 @@ +using System.Diagnostics; +using System.IO.Compression; +using System.Security.Cryptography; + +using Microsoft.Win32; + +using Bloxstrap.Enums; +using Bloxstrap.Dialogs.BootstrapperStyles; +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap +{ + public class Bootstrapper + { + private string? LaunchCommandLine; + + private string VersionGuid; + private PackageManifest VersionPackageManifest; + private FileManifest VersionFileManifest; + private string VersionFolder; + + private readonly string DownloadsFolder; + private readonly bool FreshInstall; + + private int ProgressIncrement; + private bool CancelFired = false; + + private static readonly HttpClient Client = new(); + + // 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\" }, + + { "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 readonly string AppSettings = + "\n" + + "\n" + + " content\n" + + " http://www.roblox.com\n" + + "\n"; + + public event EventHandler PromptShutdownEvent; + public event ChangeEventHandler ShowSuccessEvent; + public event ChangeEventHandler MessageChanged; + public event ChangeEventHandler ProgressBarValueChanged; + public event ChangeEventHandler ProgressBarStyleChanged; + public event ChangeEventHandler CancelEnabledChanged; + + private string _message; + private int _progress = 0; + private ProgressBarStyle _progressStyle = ProgressBarStyle.Marquee; + private bool _cancelEnabled = false; + + public string Message + { + get => _message; + + private set + { + if (_message == value) + return; + + MessageChanged.Invoke(this, new ChangeEventArgs(value)); + + _message = value; + } + } + + public int Progress + { + get => _progress; + + private set + { + if (_progress == value) + return; + + ProgressBarValueChanged.Invoke(this, new ChangeEventArgs(value)); + + _progress = value; + } + } + + public ProgressBarStyle ProgressStyle + { + get => _progressStyle; + + private set + { + if (_progressStyle == value) + return; + + ProgressBarStyleChanged.Invoke(this, new ChangeEventArgs(value)); + + _progressStyle = value; + } + } + + public bool CancelEnabled + { + get => _cancelEnabled; + + private set + { + if (_cancelEnabled == value) + return; + + CancelEnabledChanged.Invoke(this, new ChangeEventArgs(value)); + + _cancelEnabled = value; + } + } + + public Bootstrapper(BootstrapperStyle bootstrapperStyle, string? launchCommandLine = null) + { + Debug.WriteLine("Initializing bootstrapper"); + + FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid); + LaunchCommandLine = launchCommandLine; + DownloadsFolder = Path.Combine(Program.BaseDirectory, "Downloads"); + Client.Timeout = TimeSpan.FromMinutes(10); + + switch (bootstrapperStyle) + { + case BootstrapperStyle.TaskDialog: + new TaskDialogStyle(this); + break; + + case BootstrapperStyle.LegacyDialog: + Application.Run(new LegacyDialogStyle(this)); + break; + + case BootstrapperStyle.ProgressDialog: + Application.Run(new ProgressDialogStyle(this)); + break; + } + } + + public async Task Run() + { + if (LaunchCommandLine == "-uninstall") + { + Uninstall(); + return; + } + + await CheckLatestVersion(); + + if (!Directory.Exists(VersionFolder) || Program.Settings.VersionGuid != VersionGuid) + { + Debug.WriteLineIf(!Directory.Exists(VersionFolder), $"Installing latest version (!Directory.Exists({VersionFolder}))"); + Debug.WriteLineIf(Program.Settings.VersionGuid != VersionGuid, $"Installing latest version ({Program.Settings.VersionGuid} != {VersionGuid})"); + + await InstallLatestVersion(); + } + + // yes, doing this for every start is stupid, but the death sound mod is dynamically toggleable after all + ApplyModifications(); + + if (Program.IsFirstRun) + Program.SettingsManager.ShouldSave = true; + + if (Program.IsFirstRun || FreshInstall) + Register(); + + CheckInstall(); + + await StartRoblox(); + } + + private void CheckIfRunning() + { + Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta"); + + if (processes.Length > 0) + PromptShutdown(); + + try + { + // try/catch just in case process was closed before prompt was answered + + foreach (Process process in processes) + { + process.CloseMainWindow(); + process.Close(); + } + } + catch (Exception) { } + } + + private async Task StartRoblox() + { + string startEventName = Program.ProjectName.Replace(" ", "") + "StartEvent"; + + Message = "Starting Roblox..."; + + // launch time isn't really required for all launches, but it's usually just safest to do this + LaunchCommandLine += " --launchtime=" + DateTimeOffset.Now.ToUnixTimeSeconds() + " -startEvent " + startEventName; + Debug.WriteLine($"Starting game client with command line '{LaunchCommandLine}'"); + + using (SystemEvent startEvent = new(startEventName)) + { + Process.Start(Path.Combine(VersionFolder, "RobloxPlayerBeta.exe"), LaunchCommandLine); + + Debug.WriteLine($"Waiting for {startEventName} event to be fired..."); + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (startEventFired) + { + Debug.WriteLine($"{startEventName} event fired! Exiting in 5 seconds..."); + await Task.Delay(5000); + + Program.Exit(); + } + } + } + + // Bootstrapper Installing + + public static void Register() + { + RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{Program.ProjectName}"); + + // new install location selected, delete old one + string? oldInstallLocation = (string?)applicationKey.GetValue("OldInstallLocation"); + if (!String.IsNullOrEmpty(oldInstallLocation) && oldInstallLocation != Program.BaseDirectory) + { + try + { + if (Directory.Exists(oldInstallLocation)) + Directory.Delete(oldInstallLocation, true); + } + catch (Exception) { } + + applicationKey.DeleteValue("OldInstallLocation"); + } + + applicationKey.SetValue("InstallLocation", Program.BaseDirectory); + applicationKey.Close(); + + // set uninstall key + RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); + uninstallKey.SetValue("DisplayIcon", $"{Program.FilePath},0"); + uninstallKey.SetValue("DisplayName", Program.ProjectName); + uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); + uninstallKey.SetValue("InstallLocation", Program.BaseDirectory); + // uninstallKey.SetValue("NoModify", 1); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", Program.ProjectName); + uninstallKey.SetValue("ModifyPath", $"\"{Program.FilePath}\" -preferences"); + uninstallKey.SetValue("UninstallString", $"\"{Program.FilePath}\" -uninstall"); + uninstallKey.Close(); + } + + public static void CheckInstall() + { + // check if launch uri is set to our bootstrapper + // this doesn't go under register, so we check every launch + // just in case the stock bootstrapper changes it back + + Protocol.Register("roblox", "Roblox", Program.FilePath); + Protocol.Register("roblox-player", "Roblox", Program.FilePath); + + // in case the user is reinstalling + if (File.Exists(Program.FilePath) && Program.IsFirstRun) + File.Delete(Program.FilePath); + + // check to make sure bootstrapper is in the install folder + if (!File.Exists(Program.FilePath) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Program.FilePath); + } + + private void Uninstall() + { + CheckIfRunning(); + + // lots of try/catches here... lol + + Message = $"Uninstalling {Program.ProjectName}..."; + + Program.SettingsManager.ShouldSave = false; + + // check if stock bootstrapper is still installed + RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + if (bootstrapperKey is null) + { + Protocol.Unregister("roblox"); + Protocol.Unregister("roblox-player"); + } + else + { + // revert launch uri handler to stock bootstrapper + + string bootstrapperLocation = (string?)bootstrapperKey.GetValue("InstallLocation") + "RobloxPlayerLauncher.exe"; + + Protocol.Register("roblox", "Roblox", bootstrapperLocation); + Protocol.Register("roblox-player", "Roblox", bootstrapperLocation); + } + + try + { + // delete application key + Registry.CurrentUser.DeleteSubKey($@"Software\{Program.ProjectName}"); + } + catch (Exception) { } + + try + { + // delete installation folder + // (should delete everything except bloxstrap itself) + Directory.Delete(Program.BaseDirectory, true); + } + catch (Exception) { } + + try + { + // delete uninstall key + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); + } + catch (Exception) { } + + ShowSuccess($"{Program.ProjectName} has been uninstalled"); + Program.Exit(); + } + + // Roblox Installing + + private async Task CheckLatestVersion() + { + Message = "Connecting to Roblox..."; + + Debug.WriteLine($"Checking latest version..."); + VersionGuid = await Client.GetStringAsync($"{Program.BaseUrlSetup}/version"); + VersionFolder = Path.Combine(Program.BaseDirectory, "Versions", VersionGuid); + Debug.WriteLine($"Latest version is {VersionGuid}"); + + Debug.WriteLine("Getting package manifest..."); + VersionPackageManifest = await PackageManifest.Get(VersionGuid); + + Debug.WriteLine("Getting file manifest..."); + VersionFileManifest = await FileManifest.Get(VersionGuid); + } + + private async Task InstallLatestVersion() + { + CheckIfRunning(); + + if (FreshInstall) + Message = "Installing Roblox..."; + else + Message = "Upgrading Roblox..."; + + Directory.CreateDirectory(Program.BaseDirectory); + + CancelEnabled = true; + + // i believe the original bootstrapper bases the progress bar off zip + // extraction progress, but here i'm doing package download progress + + ProgressStyle = ProgressBarStyle.Continuous; + + ProgressIncrement = (int)Math.Floor((decimal) 1 / VersionPackageManifest.Count * 100); + Debug.WriteLine($"Progress Increment is {ProgressIncrement}"); + + Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Downloads")); + + foreach (Package package in VersionPackageManifest) + { + // no await, download all the packages at once + DownloadPackage(package); + } + + do + { + // wait for download to finish (and also round off the progress bar if needed) + + if (Progress == ProgressIncrement * VersionPackageManifest.Count) + Progress = 100; + + await Task.Delay(1000); + } + while (Progress != 100); + + ProgressStyle = ProgressBarStyle.Marquee; + + Debug.WriteLine("Finished downloading"); + + Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Versions")); + + foreach (Package package in VersionPackageManifest) + { + // extract all the packages at once (shouldn't be too heavy on cpu?) + ExtractPackage(package); + } + + Debug.WriteLine("Finished extracting packages"); + + Message = "Configuring Roblox..."; + + string appSettingsLocation = Path.Combine(VersionFolder, "AppSettings.xml"); + await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + + if (!FreshInstall) + { + // let's take this opportunity to delete any packages we don't need anymore + foreach (string filename in Directory.GetFiles(DownloadsFolder)) + { + if (!VersionPackageManifest.Exists(package => filename.Contains(package.Signature))) + File.Delete(filename); + } + + // and also to delete our old version folder + Directory.Delete(Path.Combine(Program.BaseDirectory, "Versions", Program.Settings.VersionGuid)); + } + + CancelEnabled = false; + + Program.Settings.VersionGuid = VersionGuid; + } + + private async void ApplyModifications() + { + // i guess we can just assume that if the hash does not match the manifest, then it's a mod + // probably not the best way to do this? don't think file corruption is that much of a worry here + + // TODO - i'm thinking i could have a manifest on my website like rbxManifest.txt + // for integrity checking and to quickly fix/alter stuff (like ouch.ogg being renamed) + // but that probably wouldn't be great to check on every run in case my webserver ever goes down + // interesting idea nonetheless, might add it sometime + + // TODO - i'm hoping i can take this idea of content mods much further + // for stuff like easily installing (community-created?) texture/shader/audio mods + // but for now, let's just keep it at this + + string fileContentName = "ouch.ogg"; + string fileContentLocation = "content\\sounds\\ouch.ogg"; + string fileLocation = Path.Combine(VersionFolder, fileContentLocation); + + string officialDeathSoundHash = VersionFileManifest[fileContentLocation]; + string currentDeathSoundHash = CalculateMD5(fileLocation); + + if (Program.Settings.UseOldDeathSound && currentDeathSoundHash == officialDeathSoundHash) + { + // let's get the old one! + + Debug.WriteLine($"Fetching old death sound..."); + + var response = await Client.GetAsync($"{Program.BaseUrlApplication}/mods/{fileContentLocation}"); + + if (File.Exists(fileLocation)) + File.Delete(fileLocation); + + using (var fileStream = new FileStream(fileLocation, FileMode.CreateNew)) + { + await response.Content.CopyToAsync(fileStream); + } + } + else if (!Program.Settings.UseOldDeathSound && currentDeathSoundHash != officialDeathSoundHash) + { + // who's lame enough to ever do this? + // well, we need to re-extract the one that's in the content-sounds.zip package + + Debug.WriteLine("Fetching current death sound..."); + + var package = VersionPackageManifest.Find(x => x.Name == "content-sounds.zip"); + + if (package is null) + { + Debug.WriteLine("Failed to find content-sounds.zip package! Aborting..."); + return; + } + + DownloadPackage(package); + + string packageLocation = Path.Combine(DownloadsFolder, package.Signature); + string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); + + using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) + { + ZipArchiveEntry? entry = archive.Entries.Where(x => x.FullName == fileContentName).FirstOrDefault(); + + if (entry is null) + { + Debug.WriteLine("Failed to find file entry in content-sounds.zip! Aborting..."); + return; + } + + if (File.Exists(fileLocation)) + File.Delete(fileLocation); + + entry.ExtractToFile(fileLocation); + } + } + } + + private async void DownloadPackage(Package package) + { + string packageUrl = $"{Program.BaseUrlSetup}/{VersionGuid}-{package.Name}"; + string packageLocation = Path.Combine(DownloadsFolder, package.Signature); + string robloxPackageLocation = Path.Combine(Program.LocalAppData, "Roblox", "Downloads", package.Signature); + + if (File.Exists(packageLocation)) + { + FileInfo file = new(packageLocation); + + string calculatedMD5 = CalculateMD5(packageLocation); + if (calculatedMD5 != package.Signature) + { + Debug.WriteLine($"{package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); + file.Delete(); + } + else + { + Debug.WriteLine($"{package.Name} is already downloaded, skipping..."); + Progress += ProgressIncrement; + return; + } + } + else if (File.Exists(robloxPackageLocation)) + { + // let's cheat! if the stock bootstrapper already previously downloaded the file, + // then we can just copy the one from there + + Debug.WriteLine($"Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); + File.Copy(robloxPackageLocation, packageLocation); + Progress += ProgressIncrement; + return; + } + + if (!File.Exists(packageLocation)) + { + Debug.WriteLine($"Downloading {package.Name}..."); + + var response = await Client.GetAsync(packageUrl); + + if (CancelFired) + return; + + using (var fileStream = new FileStream(packageLocation, FileMode.CreateNew)) + { + await response.Content.CopyToAsync(fileStream); + } + + Debug.WriteLine($"Finished downloading {package.Name}!"); + Progress += ProgressIncrement; + } + } + + private void ExtractPackage(Package package) + { + if (CancelFired) + return; + + string packageLocation = Path.Combine(DownloadsFolder, package.Signature); + string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); + string extractPath; + + Debug.WriteLine($"Extracting {package.Name} to {packageFolder}..."); + + using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (CancelFired) + return; + + if (entry.FullName.EndsWith(@"\")) + continue; + + extractPath = Path.Combine(packageFolder, entry.FullName); + + Debug.WriteLine($"[{package.Name}] Writing {extractPath}..."); + + Directory.CreateDirectory(Path.GetDirectoryName(extractPath)); + + if (File.Exists(extractPath)) + File.Delete(extractPath); + + entry.ExtractToFile(extractPath); + } + } + } + + // Dialog Events + + public void CancelButtonClicked() + { + CancelFired = true; + + try + { + if (Program.IsFirstRun) + Directory.Delete(Program.BaseDirectory, true); + else if (Directory.Exists(VersionFolder)) + Directory.Delete(VersionFolder, true); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to cleanup install!\n\n{ex}"); + } + + Program.Exit(); + } + + private void ShowSuccess(string message) + { + ShowSuccessEvent.Invoke(this, new ChangeEventArgs(message)); + } + + private void PromptShutdown() + { + PromptShutdownEvent.Invoke(this, new EventArgs()); + } + + // Utilities + + private static string CalculateMD5(string filename) + { + using (MD5 md5 = MD5.Create()) + { + using (FileStream stream = File.OpenRead(filename)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + } +} diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs new file mode 100644 index 00000000..2da9d576 --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs @@ -0,0 +1,108 @@ +namespace Bloxstrap.Dialogs.BootstrapperStyles +{ + partial class LegacyDialogStyle + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Message = new System.Windows.Forms.Label(); + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.IconBox = new System.Windows.Forms.PictureBox(); + this.CancelButton = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit(); + this.SuspendLayout(); + // + // Message + // + this.Message.Location = new System.Drawing.Point(55, 23); + this.Message.Name = "Message"; + this.Message.Size = new System.Drawing.Size(287, 17); + this.Message.TabIndex = 0; + this.Message.Text = "Please wait..."; + // + // ProgressBar + // + this.ProgressBar.Location = new System.Drawing.Point(58, 51); + this.ProgressBar.MarqueeAnimationSpeed = 33; + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(287, 26); + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.ProgressBar.TabIndex = 1; + // + // IconBox + // + this.IconBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom; + this.IconBox.ImageLocation = ""; + this.IconBox.Location = new System.Drawing.Point(19, 16); + this.IconBox.Name = "IconBox"; + this.IconBox.Size = new System.Drawing.Size(32, 32); + this.IconBox.TabIndex = 2; + this.IconBox.TabStop = false; + // + // CancelButton + // + this.CancelButton.Enabled = false; + this.CancelButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.CancelButton.Location = new System.Drawing.Point(271, 83); + this.CancelButton.Name = "CancelButton"; + this.CancelButton.Size = new System.Drawing.Size(75, 23); + this.CancelButton.TabIndex = 3; + this.CancelButton.Text = "Cancel"; + this.CancelButton.UseVisualStyleBackColor = true; + this.CancelButton.Visible = false; + this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); + // + // LegacyDialogStyle + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(362, 131); + this.Controls.Add(this.CancelButton); + this.Controls.Add(this.IconBox); + this.Controls.Add(this.ProgressBar); + this.Controls.Add(this.Message); + this.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MaximumSize = new System.Drawing.Size(378, 170); + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(378, 170); + this.Name = "LegacyDialogStyle"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "LegacyDialogStyle"; + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private Label Message; + private ProgressBar ProgressBar; + private PictureBox IconBox; + private Button CancelButton; + } +} \ No newline at end of file diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs new file mode 100644 index 00000000..0a44dea3 --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs @@ -0,0 +1,161 @@ +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap.Dialogs.BootstrapperStyles +{ + // TODO - universal implementation for winforms-based styles? (to reduce duplicate code) + + // example: https://youtu.be/3K9oCEMHj2s?t=35 + + // so this specifically emulates the 2011 version of the legacy dialog, + // but once winforms code is cleaned up we could also do the 2009 version too + // example: https://youtu.be/VpduiruysuM?t=18 + + public partial class LegacyDialogStyle : Form + { + private Bootstrapper? Bootstrapper; + + public LegacyDialogStyle(Bootstrapper? bootstrapper = null) + { + InitializeComponent(); + + if (bootstrapper is not null) + { + Bootstrapper = bootstrapper; + Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown); + Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); + Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged); + Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler(ProgressBarValueChanged); + Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler(ProgressBarStyleChanged); + Bootstrapper.CancelEnabledChanged += new ChangeEventHandler(CancelEnabledChanged); + } + + Icon icon = IconManager.GetIconResource(); + + this.Text = Program.ProjectName; + this.Icon = icon; + this.IconBox.Image = icon.ToBitmap(); + + if (Bootstrapper is null) + { + this.Message.Text = "Click the Cancel button to return to preferences"; + this.CancelButton.Enabled = true; + this.CancelButton.Visible = true; + } + else + { + Task.Run(() => RunBootstrapper()); + } + } + + public async void RunBootstrapper() + { + try + { + await Bootstrapper.Run(); + } + catch (Exception ex) + { + // string message = String.Format("{0}: {1}", ex.GetType(), ex.Message); + string message = ex.ToString(); + ShowError(message); + + Program.Exit(); + } + } + + private void ShowError(string message) + { + MessageBox.Show( + $"An error occurred while starting Roblox\n\nDetails: {message}", + Program.ProjectName, + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + + private void ShowSuccess(object sender, ChangeEventArgs e) + { + MessageBox.Show( + e.Value, + Program.ProjectName, + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + } + + private void PromptShutdown(object? sender, EventArgs e) + { + DialogResult result = MessageBox.Show( + "Roblox is currently running, but needs to close. Would you like close Roblox now?", + Program.ProjectName, + MessageBoxButtons.OKCancel, + MessageBoxIcon.Information + ); + + if (result != DialogResult.OK) + Environment.Exit(0); + } + + private void MessageChanged(object sender, ChangeEventArgs e) + { + if (this.InvokeRequired) + { + ChangeEventHandler handler = new(MessageChanged); + this.Message.Invoke(handler, sender, e); + } + else + { + this.Message.Text = e.Value; + } + } + + private void ProgressBarValueChanged(object sender, ChangeEventArgs e) + { + if (this.ProgressBar.InvokeRequired) + { + ChangeEventHandler handler = new(ProgressBarValueChanged); + this.ProgressBar.Invoke(handler, sender, e); + } + else + { + this.ProgressBar.Value = e.Value; + } + } + + private void ProgressBarStyleChanged(object sender, ChangeEventArgs e) + { + if (this.ProgressBar.InvokeRequired) + { + ChangeEventHandler handler = new(this.ProgressBarStyleChanged); + this.ProgressBar.Invoke(handler, sender, e); + } + else + { + this.ProgressBar.Style = e.Value; + } + } + + private void CancelEnabledChanged(object sender, ChangeEventArgs e) + { + if (this.CancelButton.InvokeRequired) + { + ChangeEventHandler handler = new(CancelEnabledChanged); + this.CancelButton.Invoke(handler, sender, e); + } + else + { + this.CancelButton.Enabled = e.Value; + this.CancelButton.Visible = e.Value; + } + } + + private void CancelButton_Click(object sender, EventArgs e) + { + if (Bootstrapper is null) + this.Close(); + else + Task.Run(() => Bootstrapper.CancelButtonClicked()); + } + } +} diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.resx b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.resx new file mode 100644 index 00000000..f298a7be --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.Designer.cs b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.Designer.cs new file mode 100644 index 00000000..6f8de52a --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.Designer.cs @@ -0,0 +1,126 @@ +namespace Bloxstrap.Dialogs.BootstrapperStyles +{ + partial class ProgressDialogStyle + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.Message = new System.Windows.Forms.Label(); + this.IconBox = new System.Windows.Forms.PictureBox(); + this.CancelButton = new System.Windows.Forms.PictureBox(); + this.panel1 = new System.Windows.Forms.Panel(); + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.CancelButton)).BeginInit(); + this.panel1.SuspendLayout(); + this.SuspendLayout(); + // + // ProgressBar + // + this.ProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.ProgressBar.Location = new System.Drawing.Point(29, 241); + this.ProgressBar.MarqueeAnimationSpeed = 20; + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(460, 20); + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.ProgressBar.TabIndex = 0; + // + // Message + // + this.Message.Font = new System.Drawing.Font("Tahoma", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.Message.Location = new System.Drawing.Point(29, 199); + this.Message.Name = "Message"; + this.Message.Size = new System.Drawing.Size(460, 18); + this.Message.TabIndex = 1; + this.Message.Text = "Please wait..."; + this.Message.TextAlign = System.Drawing.ContentAlignment.TopCenter; + this.Message.UseMnemonic = false; + // + // IconBox + // + this.IconBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom; + this.IconBox.ImageLocation = ""; + this.IconBox.Location = new System.Drawing.Point(212, 66); + this.IconBox.Name = "IconBox"; + this.IconBox.Size = new System.Drawing.Size(92, 92); + this.IconBox.TabIndex = 2; + this.IconBox.TabStop = false; + // + // CancelButton + // + this.CancelButton.Enabled = false; + this.CancelButton.Image = global::Bloxstrap.Properties.Resources.CancelButton; + this.CancelButton.Location = new System.Drawing.Point(194, 264); + this.CancelButton.Name = "CancelButton"; + this.CancelButton.Size = new System.Drawing.Size(130, 44); + this.CancelButton.TabIndex = 3; + this.CancelButton.TabStop = false; + this.CancelButton.Visible = false; + this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); + this.CancelButton.MouseEnter += new System.EventHandler(this.CancelButton_MouseEnter); + this.CancelButton.MouseLeave += new System.EventHandler(this.CancelButton_MouseLeave); + // + // panel1 + // + this.panel1.BackColor = System.Drawing.SystemColors.Window; + this.panel1.Controls.Add(this.Message); + this.panel1.Controls.Add(this.IconBox); + this.panel1.Controls.Add(this.CancelButton); + this.panel1.Controls.Add(this.ProgressBar); + this.panel1.Location = new System.Drawing.Point(1, 1); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(518, 318); + this.panel1.TabIndex = 4; + // + // ProgressDialogStyle + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.ActiveBorder; + this.ClientSize = new System.Drawing.Size(520, 320); + this.Controls.Add(this.panel1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.MaximumSize = new System.Drawing.Size(520, 320); + this.MinimumSize = new System.Drawing.Size(520, 320); + this.Name = "ProgressDialogStyle"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "ProgressDialogStyle"; + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.CancelButton)).EndInit(); + this.panel1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private ProgressBar ProgressBar; + private Label Message; + private PictureBox IconBox; + private PictureBox CancelButton; + private Panel panel1; + } +} \ No newline at end of file diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.cs new file mode 100644 index 00000000..739b26c8 --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.cs @@ -0,0 +1,161 @@ +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap.Dialogs.BootstrapperStyles +{ + // TODO - universal implementation for winforms-based styles? (to reduce duplicate code) + + public partial class ProgressDialogStyle : Form + { + private Bootstrapper? Bootstrapper; + + public ProgressDialogStyle(Bootstrapper? bootstrapper = null) + { + InitializeComponent(); + + Bootstrapper = bootstrapper; + + this.Text = Program.ProjectName; + this.Icon = IconManager.GetIconResource(); + this.IconBox.BackgroundImage = IconManager.GetBitmapResource(); + + if (Bootstrapper is null) + { + this.Message.Text = "Click the Cancel button to return to preferences"; + this.CancelButton.Enabled = true; + this.CancelButton.Visible = true; + } + else + { + Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown); + Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); + Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged); + Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler(ProgressBarValueChanged); + Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler(ProgressBarStyleChanged); + Bootstrapper.CancelEnabledChanged += new ChangeEventHandler(CancelEnabledChanged); + + Task.Run(() => RunBootstrapper()); + } + } + + public async void RunBootstrapper() + { + try + { + await Bootstrapper.Run(); + } + catch (Exception ex) + { + // string message = String.Format("{0}: {1}", ex.GetType(), ex.Message); + string message = ex.ToString(); + ShowError(message); + + Program.Exit(); + } + } + + private void ShowError(string message) + { + MessageBox.Show( + $"An error occurred while starting Roblox\n\nDetails: {message}", + Program.ProjectName, + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + + private void ShowSuccess(object sender, ChangeEventArgs e) + { + MessageBox.Show( + e.Value, + Program.ProjectName, + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + } + + private void PromptShutdown(object? sender, EventArgs e) + { + DialogResult result = MessageBox.Show( + "Roblox is currently running, but needs to close. Would you like close Roblox now?", + Program.ProjectName, + MessageBoxButtons.OKCancel, + MessageBoxIcon.Information + ); + + if (result != DialogResult.OK) + Environment.Exit(0); + } + + private void MessageChanged(object sender, ChangeEventArgs e) + { + if (this.InvokeRequired) + { + ChangeEventHandler handler = new(MessageChanged); + this.Message.Invoke(handler, sender, e); + } + else + { + this.Message.Text = e.Value; + } + } + + private void ProgressBarValueChanged(object sender, ChangeEventArgs e) + { + if (this.ProgressBar.InvokeRequired) + { + ChangeEventHandler handler = new(ProgressBarValueChanged); + this.ProgressBar.Invoke(handler, sender, e); + } + else + { + this.ProgressBar.Value = e.Value; + } + } + + private void ProgressBarStyleChanged(object sender, ChangeEventArgs e) + { + if (this.ProgressBar.InvokeRequired) + { + ChangeEventHandler handler = new(ProgressBarStyleChanged); + this.ProgressBar.Invoke(handler, sender, e); + } + else + { + this.ProgressBar.Style = e.Value; + } + } + + private void CancelEnabledChanged(object sender, ChangeEventArgs e) + { + if (this.CancelButton.InvokeRequired) + { + ChangeEventHandler handler = new(CancelEnabledChanged); + this.CancelButton.Invoke(handler, sender, e); + } + else + { + this.CancelButton.Enabled = e.Value; + this.CancelButton.Visible = e.Value; + } + } + + private void CancelButton_Click(object sender, EventArgs e) + { + if (Bootstrapper is null) + this.Close(); + else + Task.Run(() => Bootstrapper.CancelButtonClicked()); + } + + private void CancelButton_MouseEnter(object sender, EventArgs e) + { + this.CancelButton.Image = Properties.Resources.CancelButtonHover; + } + + private void CancelButton_MouseLeave(object sender, EventArgs e) + { + this.CancelButton.Image = Properties.Resources.CancelButton; + } + } +} diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.resx b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.resx new file mode 100644 index 00000000..f298a7be --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/TaskDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/TaskDialogStyle.cs new file mode 100644 index 00000000..019325f7 --- /dev/null +++ b/Bloxstrap/Dialogs/BootstrapperStyles/TaskDialogStyle.cs @@ -0,0 +1,134 @@ +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap.Dialogs.BootstrapperStyles +{ + // example: https://youtu.be/h0_AL95Sc3o?t=48 + + // i suppose a better name for this here would be "VistaDialog" rather than "TaskDialog"? + // having this named as BootstrapperStyles.TaskDialog would conflict with Forms.TaskDialog + // so naming it VistaDialog would let us drop the ~Style suffix on every style name + + // this currently doesn't work because c# is stupid + // technically, task dialogs are treated as winforms controls, but they don't classify as winforms controls at all + // all winforms controls have the ability to be invoked from another thread, but task dialogs don't + // so we're just kind of stuck with this not working in multithreaded use + // (unless we want the bootstrapper to freeze during package extraction) + + // for now, just stick to legacydialog and progressdialog + + public class TaskDialogStyle + { + private Bootstrapper Bootstrapper; + private TaskDialogPage Dialog; + + public TaskDialogStyle(Bootstrapper bootstrapper) + { + Bootstrapper = bootstrapper; + Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); + Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged); + Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler(ProgressBarValueChanged); + Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler(ProgressBarStyleChanged); + + Dialog = new TaskDialogPage() + { + Icon = new TaskDialogIcon(IconManager.GetIconResource()), + Caption = Program.ProjectName, + Heading = "Please wait...", + + Buttons = { TaskDialogButton.Cancel }, + ProgressBar = new TaskDialogProgressBar() + { + State = TaskDialogProgressBarState.Marquee + } + }; + + Task.Run(() => RunBootstrapper()); + TaskDialog.ShowDialog(Dialog); + } + + public async void RunBootstrapper() + { + try + { + await Bootstrapper.Run(); + } + catch (Exception ex) + { + // string message = String.Format("{0}: {1}", ex.GetType(), ex.Message); + string message = ex.ToString(); + ShowError(message); + + Program.Exit(); + } + } + + public void ShowError(string message) + { + TaskDialogPage errorDialog = new() + { + Icon = TaskDialogIcon.Error, + Caption = Program.ProjectName, + Heading = "An error occurred while starting Roblox", + Buttons = { TaskDialogButton.Close }, + Expander = new TaskDialogExpander() + { + Text = message, + CollapsedButtonText = "See details", + ExpandedButtonText = "Hide details", + Position = TaskDialogExpanderPosition.AfterText + } + }; + + Dialog.Navigate(errorDialog); + Dialog = errorDialog; + } + + public void ShowSuccess(object sender, ChangeEventArgs e) + { + TaskDialogPage successDialog = new() + { + Icon = TaskDialogIcon.ShieldSuccessGreenBar, + Caption = Program.ProjectName, + Heading = e.Value + }; + + Dialog.Navigate(successDialog); + Dialog = successDialog; + } + + private void MessageChanged(object sender, ChangeEventArgs e) + { + if (Dialog is null) + return; + + Dialog.Heading = e.Value; + } + + private void ProgressBarValueChanged(object sender, ChangeEventArgs e) + { + if (Dialog is null || Dialog.ProgressBar is null) + return; + + Dialog.ProgressBar.Value = e.Value; + } + + private void ProgressBarStyleChanged(object sender, ChangeEventArgs e) + { + if (Dialog is null || Dialog.ProgressBar is null) + return; + + switch (e.Value) + { + case ProgressBarStyle.Continuous: + case ProgressBarStyle.Blocks: + Dialog.ProgressBar.State = TaskDialogProgressBarState.Normal; + break; + + case ProgressBarStyle.Marquee: + Dialog.ProgressBar.State = TaskDialogProgressBarState.Marquee; + break; + } + } + } +} diff --git a/Bloxstrap/Dialogs/Preferences.Designer.cs b/Bloxstrap/Dialogs/Preferences.Designer.cs new file mode 100644 index 00000000..c7f09023 --- /dev/null +++ b/Bloxstrap/Dialogs/Preferences.Designer.cs @@ -0,0 +1,310 @@ +namespace Bloxstrap.Dialogs +{ + partial class Preferences + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.Tabs = new System.Windows.Forms.TabControl(); + this.DialogTab = new System.Windows.Forms.TabPage(); + this.groupBox3 = new System.Windows.Forms.GroupBox(); + this.IconPreview = new System.Windows.Forms.PictureBox(); + this.IconSelection = new System.Windows.Forms.ListBox(); + this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.StyleSelection = new System.Windows.Forms.ListBox(); + this.InstallationTab = new System.Windows.Forms.TabPage(); + this.groupBox4 = new System.Windows.Forms.GroupBox(); + this.ModifyDeathSoundToggle = new System.Windows.Forms.CheckBox(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.InstallLocationBrowseButton = new System.Windows.Forms.Button(); + this.InstallLocation = new System.Windows.Forms.TextBox(); + this.SaveButton = new System.Windows.Forms.Button(); + this.panel1 = new System.Windows.Forms.Panel(); + this.label2 = new System.Windows.Forms.Label(); + this.PreviewButton = new System.Windows.Forms.Button(); + this.InstallLocationBrowseDialog = new System.Windows.Forms.FolderBrowserDialog(); + this.Tabs.SuspendLayout(); + this.DialogTab.SuspendLayout(); + this.groupBox3.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.IconPreview)).BeginInit(); + this.groupBox2.SuspendLayout(); + this.InstallationTab.SuspendLayout(); + this.groupBox4.SuspendLayout(); + this.groupBox1.SuspendLayout(); + this.panel1.SuspendLayout(); + this.SuspendLayout(); + // + // label1 + // + this.label1.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.label1.ForeColor = System.Drawing.Color.Navy; + this.label1.Location = new System.Drawing.Point(8, 9); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(237, 23); + this.label1.TabIndex = 1; + this.label1.Text = "Configure Preferences"; + // + // Tabs + // + this.Tabs.Controls.Add(this.DialogTab); + this.Tabs.Controls.Add(this.InstallationTab); + this.Tabs.Location = new System.Drawing.Point(12, 40); + this.Tabs.Name = "Tabs"; + this.Tabs.SelectedIndex = 0; + this.Tabs.Size = new System.Drawing.Size(442, 176); + this.Tabs.TabIndex = 2; + // + // DialogTab + // + this.DialogTab.Controls.Add(this.groupBox3); + this.DialogTab.Controls.Add(this.groupBox2); + this.DialogTab.Location = new System.Drawing.Point(4, 24); + this.DialogTab.Name = "DialogTab"; + this.DialogTab.Padding = new System.Windows.Forms.Padding(3); + this.DialogTab.Size = new System.Drawing.Size(434, 148); + this.DialogTab.TabIndex = 0; + this.DialogTab.Text = "Bootstrapper"; + this.DialogTab.UseVisualStyleBackColor = true; + // + // groupBox3 + // + this.groupBox3.Controls.Add(this.IconPreview); + this.groupBox3.Controls.Add(this.IconSelection); + this.groupBox3.Location = new System.Drawing.Point(192, 3); + this.groupBox3.Name = "groupBox3"; + this.groupBox3.Size = new System.Drawing.Size(235, 140); + this.groupBox3.TabIndex = 6; + this.groupBox3.TabStop = false; + this.groupBox3.Text = "Icon"; + // + // IconPreview + // + this.IconPreview.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom; + this.IconPreview.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.IconPreview.Location = new System.Drawing.Point(117, 21); + this.IconPreview.Name = "IconPreview"; + this.IconPreview.Size = new System.Drawing.Size(109, 109); + this.IconPreview.TabIndex = 3; + this.IconPreview.TabStop = false; + // + // IconSelection + // + this.IconSelection.FormattingEnabled = true; + this.IconSelection.ItemHeight = 15; + this.IconSelection.Location = new System.Drawing.Point(9, 21); + this.IconSelection.Name = "IconSelection"; + this.IconSelection.Size = new System.Drawing.Size(100, 109); + this.IconSelection.TabIndex = 4; + this.IconSelection.SelectedIndexChanged += new System.EventHandler(this.IconSelection_SelectedIndexChanged); + // + // groupBox2 + // + this.groupBox2.Controls.Add(this.StyleSelection); + this.groupBox2.Location = new System.Drawing.Point(5, 3); + this.groupBox2.Name = "groupBox2"; + this.groupBox2.Size = new System.Drawing.Size(179, 140); + this.groupBox2.TabIndex = 5; + this.groupBox2.TabStop = false; + this.groupBox2.Text = "Style"; + // + // StyleSelection + // + this.StyleSelection.FormattingEnabled = true; + this.StyleSelection.ItemHeight = 15; + this.StyleSelection.Location = new System.Drawing.Point(9, 21); + this.StyleSelection.Name = "StyleSelection"; + this.StyleSelection.Size = new System.Drawing.Size(161, 109); + this.StyleSelection.TabIndex = 3; + this.StyleSelection.SelectedIndexChanged += new System.EventHandler(this.StyleSelection_SelectedIndexChanged); + // + // InstallationTab + // + this.InstallationTab.Controls.Add(this.groupBox4); + this.InstallationTab.Controls.Add(this.groupBox1); + this.InstallationTab.Location = new System.Drawing.Point(4, 24); + this.InstallationTab.Name = "InstallationTab"; + this.InstallationTab.Padding = new System.Windows.Forms.Padding(3); + this.InstallationTab.Size = new System.Drawing.Size(434, 148); + this.InstallationTab.TabIndex = 2; + this.InstallationTab.Text = "Installation"; + this.InstallationTab.UseVisualStyleBackColor = true; + // + // groupBox4 + // + this.groupBox4.Controls.Add(this.ModifyDeathSoundToggle); + this.groupBox4.Location = new System.Drawing.Point(5, 59); + this.groupBox4.Name = "groupBox4"; + this.groupBox4.Size = new System.Drawing.Size(422, 84); + this.groupBox4.TabIndex = 2; + this.groupBox4.TabStop = false; + this.groupBox4.Text = "Modifications"; + // + // ModifyDeathSoundToggle + // + this.ModifyDeathSoundToggle.AutoSize = true; + this.ModifyDeathSoundToggle.Checked = true; + this.ModifyDeathSoundToggle.CheckState = System.Windows.Forms.CheckState.Checked; + this.ModifyDeathSoundToggle.Location = new System.Drawing.Point(9, 21); + this.ModifyDeathSoundToggle.Margin = new System.Windows.Forms.Padding(2); + this.ModifyDeathSoundToggle.Name = "ModifyDeathSoundToggle"; + this.ModifyDeathSoundToggle.Size = new System.Drawing.Size(138, 19); + this.ModifyDeathSoundToggle.TabIndex = 1; + this.ModifyDeathSoundToggle.Text = "Use Old Death Sound"; + this.ModifyDeathSoundToggle.UseVisualStyleBackColor = true; + this.ModifyDeathSoundToggle.CheckedChanged += new System.EventHandler(this.ModifyDeathSoundToggle_CheckedChanged); + // + // groupBox1 + // + this.groupBox1.Controls.Add(this.InstallLocationBrowseButton); + this.groupBox1.Controls.Add(this.InstallLocation); + this.groupBox1.Location = new System.Drawing.Point(5, 3); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(422, 53); + this.groupBox1.TabIndex = 0; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Install Location"; + // + // InstallLocationBrowseButton + // + this.InstallLocationBrowseButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.InstallLocationBrowseButton.Location = new System.Drawing.Point(328, 20); + this.InstallLocationBrowseButton.Name = "InstallLocationBrowseButton"; + this.InstallLocationBrowseButton.Size = new System.Drawing.Size(86, 25); + this.InstallLocationBrowseButton.TabIndex = 5; + this.InstallLocationBrowseButton.Text = "Browse..."; + this.InstallLocationBrowseButton.UseVisualStyleBackColor = true; + this.InstallLocationBrowseButton.Click += new System.EventHandler(this.InstallLocationBrowseButton_Click); + // + // InstallLocation + // + this.InstallLocation.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.InstallLocation.Location = new System.Drawing.Point(9, 21); + this.InstallLocation.MaxLength = 255; + this.InstallLocation.Name = "InstallLocation"; + this.InstallLocation.Size = new System.Drawing.Size(312, 23); + this.InstallLocation.TabIndex = 4; + // + // SaveButton + // + this.SaveButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.SaveButton.Location = new System.Drawing.Point(380, 9); + this.SaveButton.Name = "SaveButton"; + this.SaveButton.Size = new System.Drawing.Size(73, 23); + this.SaveButton.TabIndex = 6; + this.SaveButton.Text = "Save"; + this.SaveButton.UseVisualStyleBackColor = true; + this.SaveButton.Click += new System.EventHandler(this.SaveButton_Click); + // + // panel1 + // + this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel1.BackColor = System.Drawing.SystemColors.Control; + this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.panel1.Controls.Add(this.label2); + this.panel1.Controls.Add(this.PreviewButton); + this.panel1.Controls.Add(this.SaveButton); + this.panel1.Location = new System.Drawing.Point(-1, 227); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(466, 42); + this.panel1.TabIndex = 6; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 13); + this.label2.Margin = new System.Windows.Forms.Padding(0); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(221, 15); + this.label2.TabIndex = 6; + this.label2.Text = "made by pizzaboxer - i think this works..."; + // + // PreviewButton + // + this.PreviewButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.PreviewButton.Location = new System.Drawing.Point(297, 9); + this.PreviewButton.Name = "PreviewButton"; + this.PreviewButton.Size = new System.Drawing.Size(73, 23); + this.PreviewButton.TabIndex = 5; + this.PreviewButton.Text = "Preview"; + this.PreviewButton.UseVisualStyleBackColor = true; + this.PreviewButton.Click += new System.EventHandler(this.PreviewButton_Click); + // + // Preferences + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.Window; + this.ClientSize = new System.Drawing.Size(464, 268); + this.Controls.Add(this.panel1); + this.Controls.Add(this.Tabs); + this.Controls.Add(this.label1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "Preferences"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Preferences"; + this.Tabs.ResumeLayout(false); + this.DialogTab.ResumeLayout(false); + this.groupBox3.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.IconPreview)).EndInit(); + this.groupBox2.ResumeLayout(false); + this.InstallationTab.ResumeLayout(false); + this.groupBox4.ResumeLayout(false); + this.groupBox4.PerformLayout(); + this.groupBox1.ResumeLayout(false); + this.groupBox1.PerformLayout(); + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private Label label1; + private TabControl Tabs; + private TabPage DialogTab; + private TabPage InstallationTab; + private Button SaveButton; + private Panel panel1; + private ListBox StyleSelection; + private GroupBox groupBox1; + private Button InstallLocationBrowseButton; + private TextBox InstallLocation; + private FolderBrowserDialog InstallLocationBrowseDialog; + private GroupBox groupBox2; + private GroupBox groupBox3; + private PictureBox IconPreview; + private ListBox IconSelection; + private Button PreviewButton; + private Label label2; + private CheckBox ModifyDeathSoundToggle; + private GroupBox groupBox4; + } +} \ No newline at end of file diff --git a/Bloxstrap/Dialogs/Preferences.cs b/Bloxstrap/Dialogs/Preferences.cs new file mode 100644 index 00000000..5756dffd --- /dev/null +++ b/Bloxstrap/Dialogs/Preferences.cs @@ -0,0 +1,235 @@ +using Microsoft.Win32; + +using Bloxstrap.Helpers; +using Bloxstrap.Enums; +using Bloxstrap.Dialogs.BootstrapperStyles; + +namespace Bloxstrap.Dialogs +{ + public partial class Preferences : Form + { + private static readonly IReadOnlyDictionary SelectableStyles = new Dictionary() + { + { "Legacy (2011 - 2014)", BootstrapperStyle.LegacyDialog }, + { "Progress (~2014)", BootstrapperStyle.ProgressDialog }, + }; + + private static readonly IReadOnlyDictionary SelectableIcons = new Dictionary() + { + { "Bloxstrap", BootstrapperIcon.IconBloxstrap }, + { "2009", BootstrapperIcon.Icon2009 }, + { "2011", BootstrapperIcon.Icon2011 }, + { "Early 2015", BootstrapperIcon.IconEarly2015 }, + { "Late 2015", BootstrapperIcon.IconLate2015 }, + { "2017", BootstrapperIcon.Icon2017 }, + { "2019", BootstrapperIcon.Icon2019 }, + }; + + private bool _useOldDeathSound = true; + private BootstrapperStyle? _selectedStyle; + private BootstrapperIcon? _selectedIcon; + + private bool UseOldDeathSound + { + get => _useOldDeathSound; + + set + { + if (_useOldDeathSound == value) + return; + + _useOldDeathSound = value; + + this.ModifyDeathSoundToggle.Checked = value; + } + } + + private BootstrapperStyle SelectedStyle + { + get => (BootstrapperStyle)_selectedStyle; + + set + { + if (_selectedStyle == value) + return; + + _selectedStyle = value; + + int index = SelectableStyles.Values.ToList().IndexOf(value); + this.StyleSelection.SetSelected(index, true); + } + } + + private BootstrapperIcon SelectedIcon + { + get => (BootstrapperIcon)_selectedIcon; + + set + { + if (_selectedIcon == value) + return; + + _selectedIcon = value; + + int index = SelectableIcons.Values.ToList().IndexOf(value); + this.IconSelection.SetSelected(index, true); + this.IconPreview.BackgroundImage = IconManager.GetBitmapResource(value); + } + } + + public Preferences() + { + InitializeComponent(); + + this.Icon = Properties.Resources.IconBloxstrap_ico; + this.Text = Program.ProjectName; + + if (Program.IsFirstRun) + { + this.SaveButton.Text = "Continue"; + this.InstallLocation.Text = Path.Combine(Program.LocalAppData, Program.ProjectName); + } + else + { + this.InstallLocation.Text = Program.BaseDirectory; + } + + foreach (var style in SelectableStyles) + { + this.StyleSelection.Items.Add(style.Key); + } + + foreach (var icon in SelectableIcons) + { + this.IconSelection.Items.Add(icon.Key); + } + + UseOldDeathSound = Program.Settings.UseOldDeathSound; + SelectedStyle = Program.Settings.BootstrapperStyle; + SelectedIcon = Program.Settings.BootstrapperIcon; + } + + private void ShowDialog(MessageBoxIcon icon, string message) + { + MessageBox.Show(message, Program.ProjectName, MessageBoxButtons.OK, icon); + } + + private void InstallLocationBrowseButton_Click(object sender, EventArgs e) + { + DialogResult result = this.InstallLocationBrowseDialog.ShowDialog(); + + if (result == DialogResult.OK) + this.InstallLocation.Text = this.InstallLocationBrowseDialog.SelectedPath; + } + + private void StyleSelection_SelectedIndexChanged(object sender, EventArgs e) + { + string selected = this.StyleSelection.Text; + SelectedStyle = SelectableStyles[selected]; + } + + private void IconSelection_SelectedIndexChanged(object sender, EventArgs e) + { + string selected = this.IconSelection.Text; + SelectedIcon = SelectableIcons[selected]; + } + + private void SaveButton_Click(object sender, EventArgs e) + { + string installLocation = this.InstallLocation.Text; + + if (String.IsNullOrEmpty(installLocation)) + { + ShowDialog(MessageBoxIcon.Error, "You must set an install location"); + return; + } + + try + { + // check if we can write to the directory (a bit hacky but eh) + + string testPath = installLocation; + string testFile = Path.Combine(installLocation, "BloxstrapWriteTest.txt"); + bool testPathExists = Directory.Exists(testPath); + + if (!testPathExists) + Directory.CreateDirectory(testPath); + + File.WriteAllText(testFile, "hi"); + File.Delete(testFile); + + if (!testPathExists) + Directory.Delete(testPath); + } + catch (UnauthorizedAccessException) + { + ShowDialog(MessageBoxIcon.Error, $"{Program.ProjectName} does not have write access to the install location you selected. Please choose another install location."); + return; + } + catch (Exception ex) + { + ShowDialog(MessageBoxIcon.Error, ex.Message); + return; + } + + if (Program.IsFirstRun) + { + // this will be set in the registry after first install + Program.BaseDirectory = installLocation; + } + else if (Program.BaseDirectory != installLocation) + { + ShowDialog(MessageBoxIcon.Information, $"{Program.ProjectName} will install to the new location you've set the next time it runs."); + + Program.Settings.VersionGuid = ""; + + using (RegistryKey registryKey = Registry.CurrentUser.CreateSubKey($@"Software\{Program.ProjectName}")) + { + registryKey.SetValue("InstallLocation", installLocation); + registryKey.SetValue("OldInstallLocation", Program.BaseDirectory); + } + + // preserve settings + // we don't need to copy the bootstrapper over since the install process will do that automatically + + Program.SettingsManager.Save(); + File.Copy(Path.Combine(Program.BaseDirectory, "Settings.json"), Path.Combine(installLocation, "Settings.json")); + } + + Program.Settings.UseOldDeathSound = UseOldDeathSound; + Program.Settings.BootstrapperStyle = SelectedStyle; + Program.Settings.BootstrapperIcon = SelectedIcon; + + this.Close(); + } + + private void PreviewButton_Click(object sender, EventArgs e) + { + // small hack to get the icon to show in the preview without saving to settings + BootstrapperIcon savedIcon = Program.Settings.BootstrapperIcon; + Program.Settings.BootstrapperIcon = SelectedIcon; + + this.Visible = false; + + switch (SelectedStyle) + { + case BootstrapperStyle.LegacyDialog: + new LegacyDialogStyle().ShowDialog(); + break; + + case BootstrapperStyle.ProgressDialog: + new ProgressDialogStyle().ShowDialog(); + break; + } + + Program.Settings.BootstrapperIcon = savedIcon; + + this.Visible = true; + } + + private void ModifyDeathSoundToggle_CheckedChanged(object sender, EventArgs e) + { + UseOldDeathSound = this.ModifyDeathSoundToggle.Checked; + } + } +} diff --git a/Bloxstrap/Dialogs/Preferences.resx b/Bloxstrap/Dialogs/Preferences.resx new file mode 100644 index 00000000..8e732d63 --- /dev/null +++ b/Bloxstrap/Dialogs/Preferences.resx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/Bloxstrap/Enums/BootstrapperIcon.cs b/Bloxstrap/Enums/BootstrapperIcon.cs new file mode 100644 index 00000000..e0444d4c --- /dev/null +++ b/Bloxstrap/Enums/BootstrapperIcon.cs @@ -0,0 +1,13 @@ +namespace Bloxstrap.Enums +{ + public enum BootstrapperIcon + { + IconBloxstrap, + Icon2009, + Icon2011, + IconEarly2015, + IconLate2015, + Icon2017, + Icon2019 + } +} diff --git a/Bloxstrap/Enums/BootstrapperStyle.cs b/Bloxstrap/Enums/BootstrapperStyle.cs new file mode 100644 index 00000000..fa3e00a7 --- /dev/null +++ b/Bloxstrap/Enums/BootstrapperStyle.cs @@ -0,0 +1,9 @@ +namespace Bloxstrap.Enums +{ + public enum BootstrapperStyle + { + TaskDialog, + LegacyDialog, + ProgressDialog + } +} diff --git a/Bloxstrap/Helpers/IconManager.cs b/Bloxstrap/Helpers/IconManager.cs new file mode 100644 index 00000000..3f873a85 --- /dev/null +++ b/Bloxstrap/Helpers/IconManager.cs @@ -0,0 +1,45 @@ +using Bloxstrap.Enums; + +namespace Bloxstrap.Helpers +{ + internal class IconManager + { + public static Icon GetIconResource() + { + return GetIconResource(Program.Settings.BootstrapperIcon); + } + + public static Icon GetIconResource(BootstrapperIcon icon) + { + switch (icon) + { + case BootstrapperIcon.Icon2009: return Properties.Resources.Icon2009_ico; + case BootstrapperIcon.Icon2011: return Properties.Resources.Icon2011_ico; + case BootstrapperIcon.IconEarly2015: return Properties.Resources.IconEarly2015_ico; + case BootstrapperIcon.IconLate2015: return Properties.Resources.IconLate2015_ico; + case BootstrapperIcon.Icon2017: return Properties.Resources.Icon2017_ico; + case BootstrapperIcon.Icon2019: return Properties.Resources.Icon2019_ico; + case BootstrapperIcon.IconBloxstrap: default: return Properties.Resources.IconBloxstrap_ico; + } + } + + public static Bitmap GetBitmapResource() + { + return GetBitmapResource(Program.Settings.BootstrapperIcon); + } + + public static Bitmap GetBitmapResource(BootstrapperIcon icon) + { + switch (icon) + { + case BootstrapperIcon.Icon2009: return Properties.Resources.Icon2009_png; + case BootstrapperIcon.Icon2011: return Properties.Resources.Icon2011_png; + case BootstrapperIcon.IconEarly2015: return Properties.Resources.IconEarly2015_png; + case BootstrapperIcon.IconLate2015: return Properties.Resources.IconLate2015_png; + case BootstrapperIcon.Icon2017: return Properties.Resources.Icon2017_png; + case BootstrapperIcon.Icon2019: return Properties.Resources.Icon2019_png; + case BootstrapperIcon.IconBloxstrap: default: return Properties.Resources.IconBloxstrap_png; + } + } + } +} diff --git a/Bloxstrap/Helpers/Protocol.cs b/Bloxstrap/Helpers/Protocol.cs new file mode 100644 index 00000000..a64a9f9c --- /dev/null +++ b/Bloxstrap/Helpers/Protocol.cs @@ -0,0 +1,81 @@ +using System.Web; +using Microsoft.Win32; + +namespace Bloxstrap.Helpers +{ + public class Protocol + { + // map uri keys to command line args + private static readonly IReadOnlyDictionary UriKeyArgMap = new Dictionary() + { + // excluding roblox-player, browsertrackerid and channel + { "launchmode", "--" }, + { "gameinfo", "-t " }, + { "placelauncherurl", "-j "}, + // { "launchtime", "--launchtime=" }, we'll set this when launching the game client + { "robloxLocale", "--rloc " }, + { "gameLocale", "--gloc " }, + }; + + public static string Parse(string protocol) + { + string[] keyvalPair; + string key; + string val; + string commandLine = ""; + + foreach (var parameter in protocol.Split('+')) + { + if (!parameter.Contains(':')) + continue; + + keyvalPair = parameter.Split(':'); + key = keyvalPair[0]; + val = keyvalPair[1]; + + if (!UriKeyArgMap.ContainsKey(key)) + continue; + + if (key == "placelauncherurl") + val = HttpUtility.UrlDecode(val).Replace("browserTrackerId", "lol"); + + commandLine += UriKeyArgMap[key] + val + " "; + } + + return commandLine; + } + + 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"); + + if (uriKey.GetValue("") is null) + { + uriKey.SetValue("", $"URL: {name} Protocol"); + uriKey.SetValue("URL Protocol", ""); + } + + if ((string?)uriCommandKey.GetValue("") != handlerArgs) + { + uriIconKey.SetValue("", handler); + uriCommandKey.SetValue("", handlerArgs); + } + + uriKey.Close(); + uriIconKey.Close(); + uriCommandKey.Close(); + } + + public static void Unregister(string key) + { + try + { + Registry.CurrentUser.DeleteSubKey($@"Software\Classes\{key}"); + } + catch (Exception) { } + } + } +} diff --git a/Bloxstrap/Helpers/RSMM/ChangeEvent.cs b/Bloxstrap/Helpers/RSMM/ChangeEvent.cs new file mode 100644 index 00000000..2a6d2e0f --- /dev/null +++ b/Bloxstrap/Helpers/RSMM/ChangeEvent.cs @@ -0,0 +1,16 @@ +// https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/ProjectSrc/Events/ChangeEvent.cs + +namespace Bloxstrap.Helpers.RSMM +{ + public delegate void ChangeEventHandler(object sender, ChangeEventArgs e); + + public class ChangeEventArgs + { + public T Value { get; } + + public ChangeEventArgs(T value) + { + Value = value; + } + } +} diff --git a/Bloxstrap/Helpers/RSMM/FileManifest.cs b/Bloxstrap/Helpers/RSMM/FileManifest.cs new file mode 100644 index 00000000..4905a4fd --- /dev/null +++ b/Bloxstrap/Helpers/RSMM/FileManifest.cs @@ -0,0 +1,62 @@ +// https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/ProjectSrc/Bootstrapper/FileManifest.cs + +using System.Runtime.Serialization; + +namespace Bloxstrap.Helpers.RSMM +{ + [Serializable] + internal class FileManifest : Dictionary + { + public string RawData { get; set; } + + protected FileManifest(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + private FileManifest(string data, bool remapExtraContent = false) + { + using (var reader = new StringReader(data)) + { + bool eof = false; + + var readLine = new Func(() => + { + string line = reader.ReadLine(); + + if (line == null) + eof = true; + + return line; + }); + + while (!eof) + { + string path = readLine(); + string signature = readLine(); + + if (eof) + break; + else if (remapExtraContent && path.StartsWith("ExtraContent", Program.StringFormat)) + path = path.Replace("ExtraContent", "content"); + + Add(path, signature); + } + } + + RawData = data; + } + + public static async Task Get(string versionGuid, bool remapExtraContent = false) + { + string fileManifestUrl = $"{Program.BaseUrlSetup}/{versionGuid}-rbxManifest.txt"; + string fileManifestData; + + using (HttpClient http = new()) + { + var download = http.GetStringAsync(fileManifestUrl); + fileManifestData = await download.ConfigureAwait(false); + } + + return new FileManifest(fileManifestData, remapExtraContent); + } + } +} diff --git a/Bloxstrap/Helpers/RSMM/Package.cs b/Bloxstrap/Helpers/RSMM/Package.cs new file mode 100644 index 00000000..9df66c06 --- /dev/null +++ b/Bloxstrap/Helpers/RSMM/Package.cs @@ -0,0 +1,22 @@ +// https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/ProjectSrc/Utility/Package.cs + +namespace Bloxstrap.Helpers.RSMM +{ + internal class Package + { + public string Name { get; set; } + public string Signature { get; set; } + public int PackedSize { get; set; } + public int Size { get; set; } + + public bool Exists { get; set; } + public bool ShouldInstall { get; set; } + + internal byte[] Data { get; set; } + + public override string ToString() + { + return $"[{Signature}] {Name}"; + } + } +} diff --git a/Bloxstrap/Helpers/RSMM/PackageManifest.cs b/Bloxstrap/Helpers/RSMM/PackageManifest.cs new file mode 100644 index 00000000..b7708520 --- /dev/null +++ b/Bloxstrap/Helpers/RSMM/PackageManifest.cs @@ -0,0 +1,85 @@ +// https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/ProjectSrc/Bootstrapper/PackageManifest.cs + + +namespace Bloxstrap.Helpers.RSMM +{ + internal class PackageManifest : List + { + public string RawData { get; private set; } + + private PackageManifest(string data) + { + using (var reader = new StringReader(data)) + { + string version = reader.ReadLine(); + + if (version != "v0") + { + string errorMsg = $"Unexpected package manifest version: {version} (expected v0!)\n" + + "Please contact MaximumADHD if you see this error."; + + throw new NotSupportedException(errorMsg); + } + + bool eof = false; + + var readLine = new Func(() => + { + string line = reader.ReadLine(); + + if (line == null) + eof = true; + + return line; + }); + + while (!eof) + { + string fileName = readLine(); + string signature = readLine(); + + string rawPackedSize = readLine(); + string rawSize = readLine(); + + if (eof) + break; + + if (!int.TryParse(rawPackedSize, out int packedSize)) + break; + + if (!int.TryParse(rawSize, out int size)) + break; + + if (fileName == "RobloxPlayerLauncher.exe") + break; + + var package = new Package() + { + Name = fileName, + Signature = signature, + PackedSize = packedSize, + Size = size + }; + + Add(package); + } + } + + RawData = data; + } + + public static async Task Get(string versionGuid) + { + string pkgManifestUrl = $"{Program.BaseUrlSetup}/{versionGuid}-rbxPkgManifest.txt"; + string pkgManifestData; + + using (HttpClient http = new()) + { + var getData = http.GetStringAsync(pkgManifestUrl); + pkgManifestData = await getData.ConfigureAwait(false); + } + + return new PackageManifest(pkgManifestData); + } + } +} diff --git a/Bloxstrap/Helpers/RSMM/SystemEvent.cs b/Bloxstrap/Helpers/RSMM/SystemEvent.cs new file mode 100644 index 00000000..ce0a1914 --- /dev/null +++ b/Bloxstrap/Helpers/RSMM/SystemEvent.cs @@ -0,0 +1,39 @@ +// https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/ProjectSrc/Utility/SystemEvent.cs + +namespace Bloxstrap.Helpers.RSMM +{ + public class SystemEvent : EventWaitHandle + { + public string Name { get; private set; } + + public SystemEvent(string name, bool init = false, EventResetMode mode = EventResetMode.AutoReset) : base(init, mode, name) + { + if (init) + Reset(); + else + Set(); + + Name = name; + } + + public override string ToString() + { + return Name; + } + + public Task WaitForEvent() + { + return Task.Run(WaitOne); + } + + public Task WaitForEvent(TimeSpan timeout, bool exitContext = false) + { + return Task.Run(() => WaitOne(timeout, exitContext)); + } + + public Task WaitForEvent(int millisecondsTimeout, bool exitContext = false) + { + return Task.Run(() => WaitOne(millisecondsTimeout, exitContext)); + } + } +} diff --git a/Bloxstrap/Program.cs b/Bloxstrap/Program.cs new file mode 100644 index 00000000..b293e03c --- /dev/null +++ b/Bloxstrap/Program.cs @@ -0,0 +1,115 @@ +using Microsoft.Win32; +using System.Diagnostics; +using Bloxstrap.Helpers; + +namespace Bloxstrap +{ + internal static class Program + { + public const StringComparison StringFormat = StringComparison.InvariantCulture; + + // ideally for the application website, i would prefer something other than my own hosted website? + // i don't really have many other options though - github doesn't make much sense for something like this + + public const string ProjectName = "Bloxstrap"; + public const string ProjectRepository = "pizzaboxer/bloxstrap"; + public const string BaseUrlApplication = "https://bloxstrap.pizzaboxer.xyz"; + public const string BaseUrlSetup = "https://s3.amazonaws.com/setup.roblox.com"; + + public static string? BaseDirectory; + public static string LocalAppData { get; private set; } + public static string FilePath { get; private set; } + public static bool IsFirstRun { get; private set; } = false; + + public static SettingsFormat Settings; + public static SettingsManager SettingsManager = new(); + + public static void Exit() + { + SettingsManager.Save(); + Environment.Exit(0); + } + + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + // ensure only one is running + Process[] processes = Process.GetProcessesByName(ProjectName); + if (processes.Length > 1) + return; + + // Task.Run(() => Updater.CheckForUpdates()).Wait(); + // return; + + LocalAppData = Environment.GetEnvironmentVariable("localappdata"); + + RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"); + + if (registryKey is null) + { + IsFirstRun = true; + Settings = SettingsManager.Settings; + Application.Run(new Dialogs.Preferences()); + } + else + { + BaseDirectory = (string?)registryKey.GetValue("InstallLocation"); + registryKey.Close(); + } + + // selection dialog was closed + // (this doesnt account for the registry value not existing but thats basically never gonna happen) + if (BaseDirectory is null) + return; + + SettingsManager.SaveLocation = Path.Combine(BaseDirectory, "Settings.json"); + FilePath = Path.Combine(BaseDirectory, $"{ProjectName}.exe"); + + // we shouldn't save settings on the first run until the first installation is finished, + // just in case the user decides to cancel the install + if (!IsFirstRun) + { + Settings = SettingsManager.Settings; + SettingsManager.ShouldSave = true; + } + + string commandLine = ""; + + if (args.Length > 0) + { + if (args[0] == "-preferences") + { + Application.Run(new Dialogs.Preferences()); + } + else if (args[0].StartsWith("roblox-player:")) + { + commandLine = Protocol.Parse(args[0]); + } + else if (args[0].StartsWith("roblox:")) + { + commandLine = $"--app --deeplink {args[0]}"; + } + else + { + commandLine = String.Join(" ", args); + } + } + else + { + commandLine = "--app"; + } + + if (!String.IsNullOrEmpty(commandLine)) + new Bootstrapper(Settings.BootstrapperStyle, commandLine); + + SettingsManager.Save(); + } + } +} diff --git a/Bloxstrap/Properties/Resources.Designer.cs b/Bloxstrap/Properties/Resources.Designer.cs new file mode 100644 index 00000000..9c57c694 --- /dev/null +++ b/Bloxstrap/Properties/Resources.Designer.cs @@ -0,0 +1,223 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Bloxstrap.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Bloxstrap.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap CancelButton { + get { + object obj = ResourceManager.GetObject("CancelButton", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap CancelButtonHover { + get { + object obj = ResourceManager.GetObject("CancelButtonHover", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Icon2009_ico { + get { + object obj = ResourceManager.GetObject("Icon2009_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Icon2009_png { + get { + object obj = ResourceManager.GetObject("Icon2009_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Icon2011_ico { + get { + object obj = ResourceManager.GetObject("Icon2011_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Icon2011_png { + get { + object obj = ResourceManager.GetObject("Icon2011_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Icon2017_ico { + get { + object obj = ResourceManager.GetObject("Icon2017_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Icon2017_png { + get { + object obj = ResourceManager.GetObject("Icon2017_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Icon2019_ico { + get { + object obj = ResourceManager.GetObject("Icon2019_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Icon2019_png { + get { + object obj = ResourceManager.GetObject("Icon2019_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconBloxstrap_ico { + get { + object obj = ResourceManager.GetObject("IconBloxstrap_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconBloxstrap_png { + get { + object obj = ResourceManager.GetObject("IconBloxstrap_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconEarly2015_ico { + get { + object obj = ResourceManager.GetObject("IconEarly2015_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconEarly2015_png { + get { + object obj = ResourceManager.GetObject("IconEarly2015_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconLate2015_ico { + get { + object obj = ResourceManager.GetObject("IconLate2015_ico", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconLate2015_png { + get { + object obj = ResourceManager.GetObject("IconLate2015_png", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Bloxstrap/Properties/Resources.resx b/Bloxstrap/Properties/Resources.resx new file mode 100644 index 00000000..924339dc --- /dev/null +++ b/Bloxstrap/Properties/Resources.resx @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\CancelButton.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\CancelButtonHover.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2009-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2009-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2011-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2011-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2017-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2017-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2019-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Icon2019-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\IconBloxstrap-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\IconBloxstrap-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\IconEarly2015-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\IconEarly2015-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\IconLate2015-ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\IconLate2015-png.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Bloxstrap/Properties/Settings.Designer.cs b/Bloxstrap/Properties/Settings.Designer.cs new file mode 100644 index 00000000..0e376d22 --- /dev/null +++ b/Bloxstrap/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Bloxstrap.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.0.3.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Bloxstrap/Properties/Settings.settings b/Bloxstrap/Properties/Settings.settings new file mode 100644 index 00000000..049245f4 --- /dev/null +++ b/Bloxstrap/Properties/Settings.settings @@ -0,0 +1,6 @@ + + + + + + diff --git a/Bloxstrap/Resources/CancelButton.png b/Bloxstrap/Resources/CancelButton.png new file mode 100644 index 00000000..966103bf Binary files /dev/null and b/Bloxstrap/Resources/CancelButton.png differ diff --git a/Bloxstrap/Resources/CancelButtonHover.png b/Bloxstrap/Resources/CancelButtonHover.png new file mode 100644 index 00000000..80b79a44 Binary files /dev/null and b/Bloxstrap/Resources/CancelButtonHover.png differ diff --git a/Bloxstrap/Resources/Icon2009-ico.ico b/Bloxstrap/Resources/Icon2009-ico.ico new file mode 100644 index 00000000..34164b36 Binary files /dev/null and b/Bloxstrap/Resources/Icon2009-ico.ico differ diff --git a/Bloxstrap/Resources/Icon2009-png.png b/Bloxstrap/Resources/Icon2009-png.png new file mode 100644 index 00000000..f3d71adf Binary files /dev/null and b/Bloxstrap/Resources/Icon2009-png.png differ diff --git a/Bloxstrap/Resources/Icon2011-ico.ico b/Bloxstrap/Resources/Icon2011-ico.ico new file mode 100644 index 00000000..486488f3 Binary files /dev/null and b/Bloxstrap/Resources/Icon2011-ico.ico differ diff --git a/Bloxstrap/Resources/Icon2011-png.png b/Bloxstrap/Resources/Icon2011-png.png new file mode 100644 index 00000000..0b1d0f34 Binary files /dev/null and b/Bloxstrap/Resources/Icon2011-png.png differ diff --git a/Bloxstrap/Resources/Icon2017-ico.ico b/Bloxstrap/Resources/Icon2017-ico.ico new file mode 100644 index 00000000..67a46ad9 Binary files /dev/null and b/Bloxstrap/Resources/Icon2017-ico.ico differ diff --git a/Bloxstrap/Resources/Icon2017-png.png b/Bloxstrap/Resources/Icon2017-png.png new file mode 100644 index 00000000..a589af24 Binary files /dev/null and b/Bloxstrap/Resources/Icon2017-png.png differ diff --git a/Bloxstrap/Resources/Icon2019-ico.ico b/Bloxstrap/Resources/Icon2019-ico.ico new file mode 100644 index 00000000..9424b47f Binary files /dev/null and b/Bloxstrap/Resources/Icon2019-ico.ico differ diff --git a/Bloxstrap/Resources/Icon2019-png.png b/Bloxstrap/Resources/Icon2019-png.png new file mode 100644 index 00000000..99cf2881 Binary files /dev/null and b/Bloxstrap/Resources/Icon2019-png.png differ diff --git a/Bloxstrap/Resources/IconBloxstrap-ico.ico b/Bloxstrap/Resources/IconBloxstrap-ico.ico new file mode 100644 index 00000000..3d75f694 Binary files /dev/null and b/Bloxstrap/Resources/IconBloxstrap-ico.ico differ diff --git a/Bloxstrap/Resources/IconBloxstrap-png.png b/Bloxstrap/Resources/IconBloxstrap-png.png new file mode 100644 index 00000000..ed9c2a76 Binary files /dev/null and b/Bloxstrap/Resources/IconBloxstrap-png.png differ diff --git a/Bloxstrap/Resources/IconEarly2015-ico.ico b/Bloxstrap/Resources/IconEarly2015-ico.ico new file mode 100644 index 00000000..22b99833 Binary files /dev/null and b/Bloxstrap/Resources/IconEarly2015-ico.ico differ diff --git a/Bloxstrap/Resources/IconEarly2015-png.png b/Bloxstrap/Resources/IconEarly2015-png.png new file mode 100644 index 00000000..0e662509 Binary files /dev/null and b/Bloxstrap/Resources/IconEarly2015-png.png differ diff --git a/Bloxstrap/Resources/IconLate2015-ico.ico b/Bloxstrap/Resources/IconLate2015-ico.ico new file mode 100644 index 00000000..8b9eb52d Binary files /dev/null and b/Bloxstrap/Resources/IconLate2015-ico.ico differ diff --git a/Bloxstrap/Resources/IconLate2015-png.png b/Bloxstrap/Resources/IconLate2015-png.png new file mode 100644 index 00000000..316bf535 Binary files /dev/null and b/Bloxstrap/Resources/IconLate2015-png.png differ diff --git a/Bloxstrap/Settings.cs b/Bloxstrap/Settings.cs new file mode 100644 index 00000000..ede8d381 --- /dev/null +++ b/Bloxstrap/Settings.cs @@ -0,0 +1,68 @@ +using System.Diagnostics; +using System.Text.Json; +using Bloxstrap.Enums; + +namespace Bloxstrap +{ + public class SettingsFormat + { + public string VersionGuid { get; set; } + public bool UseOldDeathSound { get; set; } = true; + public BootstrapperStyle BootstrapperStyle { get; set; } = BootstrapperStyle.ProgressDialog; + public BootstrapperIcon BootstrapperIcon { get; set; } = BootstrapperIcon.IconBloxstrap; + } + + public class SettingsManager + { + public SettingsFormat Settings = new(); + public bool ShouldSave = false; + + private string _saveLocation; + public string SaveLocation + { + get => _saveLocation; + + set + { + if (!String.IsNullOrEmpty(_saveLocation)) + return; + + _saveLocation = value; + + string settingsJson = ""; + + if (File.Exists(_saveLocation)) + settingsJson = File.ReadAllText(_saveLocation); + + Debug.WriteLine(settingsJson); + + try + { + Settings = JsonSerializer.Deserialize(settingsJson); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to fetch settings! Reverting to defaults... ({ex.Message})"); + // Settings = new(); + } + } + } + + public void Save() + { + Debug.WriteLine("Attempting to save..."); + + string SettingsJson = JsonSerializer.Serialize(Settings, new JsonSerializerOptions { WriteIndented = true }); + Debug.WriteLine(SettingsJson); + + if (!ShouldSave) + { + Debug.WriteLine("ShouldSave set to false, not saving..."); + return; + } + + // save settings + File.WriteAllText(SaveLocation, SettingsJson); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..7d17be04 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Bloxstrap +An open, customizable, feature-packed alternative bootstrapper for Roblox. + +## Usage +This is intended to be a seamless replacement for the stock Roblox bootstrapper, working more or less how you'd expect it to. + +When you first run it, you'll see the preferences menu. If you want to configure preferences again after you've already installed it, open up Apps and Features, look for Bloxstrap and select Modify. + +Bloxstrap can be uninstalled, and reverts to the stock boostrapper when uninstalled, so it's relatively harmless. + +Please keep in mind that **Bloxstrap is in very early development**, and you'll no doubt encounter some bugs. If you do, please submit an issue! + +## Features +Here's some of the features that Bloxstrap provides over the stock Roblox bootstrapper: + +* Bootstrapper style can be customized (including being able to emulate older version looks) +* Install location can be selected (useful if you usually install all your games on a separate drive) +* Support for the old death sound (no messing around with your install folder, and it persists on updates!) + +Some more features that we hope to add include more custom styles, and Discord Rich Presence support. + +## Installing +Bloxstrap requires the [x86 .NET 6 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.7-windows-x86-installer). If you don't already have it installed, you'll be prompted to install it when running Bloxstrap. + +As of right now, Bloxstrap is only supported for Windows. Mac support is possible, but I don't currently have a Mac to test this on. + +## Contributions +* [Roblox Studio Mod Manager](/MaximumADHD/Roblox-Studio-Mod-Manager) by [MaximumADHD](https://www.roblox.com/users/2032622/profile) - some utility code was borrowed to help with Bloxstrap's bootstrapper functionality. They're all under Bloxstrap.Helpers.RSMM, with some having slight modifications. Besides, it's a great project. +* [skulyire](https://www.roblox.com/users/2485612194/profile) - Making the Bloxstrap logo \ No newline at end of file