Features and bugfixes for v1.1.0

- Features
    - Add Discord Rich Presence support (the nuget package is like a year and a half out of date so submodule it is lol)
    - Add update checker
    - Add start menu folder creation

- Bugfixes
   - Fix "Directory is not empty" error when updating Roblox
   - Fix uninstalling sometimes not working properly

- Quality of Life
   - Split Bootstrapper class into partial files
   - Renamed TaskDialogStyle to VistaDialog for name simplification
This commit is contained in:
pizzaboxer 2022-08-11 08:26:28 +01:00
parent f06e38eddb
commit bacb650ddc
26 changed files with 1177 additions and 829 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "DiscordRPC"]
path = DiscordRPC
url = https://github.com/Lachee/discord-rpc-csharp.git

View File

@ -5,6 +5,8 @@ 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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRPC", "DiscordRPC\DiscordRPC\DiscordRPC.csproj", "{BDB66971-35FA-45BD-ABD6-70B814D2E55C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{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
{BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -9,14 +9,23 @@
<PlatformTarget>AnyCPU</PlatformTarget>
<Platforms>AnyCPU;x86</Platforms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>1.0.0</Version>
<FileVersion>1.0.0.0</FileVersion>
<Version>1.1.0</Version>
<FileVersion>1.1.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="Bloxstrap.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DiscordRPC\DiscordRPC\DiscordRPC.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>

View File

@ -1,655 +0,0 @@
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<string, string> PackageDirectories = new Dictionary<string, string>()
{
{ "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 =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Settings>\n" +
" <ContentFolder>content</ContentFolder>\n" +
" <BaseUrl>http://www.roblox.com</BaseUrl>\n" +
"</Settings>\n";
public event EventHandler PromptShutdownEvent;
public event ChangeEventHandler<string> ShowSuccessEvent;
public event ChangeEventHandler<string> MessageChanged;
public event ChangeEventHandler<int> ProgressBarValueChanged;
public event ChangeEventHandler<ProgressBarStyle> ProgressBarStyleChanged;
public event ChangeEventHandler<bool> 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<string>(value));
_message = value;
}
}
public int Progress
{
get => _progress;
private set
{
if (_progress == value)
return;
ProgressBarValueChanged.Invoke(this, new ChangeEventArgs<int>(value));
_progress = value;
}
}
public ProgressBarStyle ProgressStyle
{
get => _progressStyle;
private set
{
if (_progressStyle == value)
return;
ProgressBarStyleChanged.Invoke(this, new ChangeEventArgs<ProgressBarStyle>(value));
_progressStyle = value;
}
}
public bool CancelEnabled
{
get => _cancelEnabled;
private set
{
if (_cancelEnabled == value)
return;
CancelEnabledChanged.Invoke(this, new ChangeEventArgs<bool>(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<string>(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();
}
}
}
}
}

View File

@ -0,0 +1,128 @@
using Microsoft.Win32;
using Bloxstrap.Helpers;
namespace Bloxstrap
{
partial class Bootstrapper
{
public static void Register()
{
if (Program.BaseDirectory is null)
return;
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("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);
// this SHOULD go under Register(),
// but then people who have Bloxstrap v1.0.0 installed won't have this without a reinstall
// maybe in a later version?
if (!Directory.Exists(Program.StartMenuDirectory))
{
Directory.CreateDirectory(Program.StartMenuDirectory);
ShellLink.Shortcut.CreateShortcut(Program.FilePath, "", Program.FilePath, 0)
.WriteToFile(Path.Combine(Program.StartMenuDirectory, "Play Roblox.lnk"));
ShellLink.Shortcut.CreateShortcut(Program.FilePath, "-preferences", Program.FilePath, 0)
.WriteToFile(Path.Combine(Program.StartMenuDirectory, "Configure Bloxstrap.lnk"));
}
}
private void Uninstall()
{
if (Program.BaseDirectory is null)
return;
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}");
// delete start menu folder
Directory.Delete(Program.StartMenuDirectory, true);
// delete uninstall key
Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}");
// delete installation folder
// (should delete everything except bloxstrap itself)
Directory.Delete(Program.BaseDirectory, true);
}
catch (Exception) { }
ShowSuccess($"{Program.ProjectName} has been uninstalled");
Program.Exit();
}
}
}

View File

@ -0,0 +1,130 @@
using Bloxstrap.Helpers.RSMM;
namespace Bloxstrap
{
partial 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<string, string> PackageDirectories = new Dictionary<string, string>()
{
{ "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 =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Settings>\n" +
" <ContentFolder>content</ContentFolder>\n" +
" <BaseUrl>http://www.roblox.com</BaseUrl>\n" +
"</Settings>\n";
public event EventHandler CloseDialogEvent;
public event EventHandler PromptShutdownEvent;
public event ChangeEventHandler<string> ShowSuccessEvent;
public event ChangeEventHandler<string> MessageChanged;
public event ChangeEventHandler<int> ProgressBarValueChanged;
public event ChangeEventHandler<ProgressBarStyle> ProgressBarStyleChanged;
public event ChangeEventHandler<bool> 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<string>(value));
_message = value;
}
}
public int Progress
{
get => _progress;
private set
{
if (_progress == value)
return;
ProgressBarValueChanged.Invoke(this, new ChangeEventArgs<int>(value));
_progress = value;
}
}
public ProgressBarStyle ProgressStyle
{
get => _progressStyle;
private set
{
if (_progressStyle == value)
return;
ProgressBarStyleChanged.Invoke(this, new ChangeEventArgs<ProgressBarStyle>(value));
_progressStyle = value;
}
}
public bool CancelEnabled
{
get => _cancelEnabled;
private set
{
if (_cancelEnabled == value)
return;
CancelEnabledChanged.Invoke(this, new ChangeEventArgs<bool>(value));
_cancelEnabled = value;
}
}
}
}

View File

@ -0,0 +1,208 @@
using System.Diagnostics;
using System.IO.Compression;
using Bloxstrap.Helpers;
using Bloxstrap.Helpers.RSMM;
namespace Bloxstrap
{
partial class Bootstrapper
{
private async Task CheckLatestVersion()
{
if (Program.BaseDirectory is null)
return;
Message = "Connecting to Roblox...";
VersionGuid = await Client.GetStringAsync($"{Program.BaseUrlSetup}/version");
VersionFolder = Path.Combine(Program.BaseDirectory, "Versions", VersionGuid);
VersionPackageManifest = await PackageManifest.Get(VersionGuid);
VersionFileManifest = await FileManifest.Get(VersionGuid);
}
private async Task InstallLatestVersion()
{
if (Program.BaseDirectory is null)
return;
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);
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), true);
}
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
await ModifyDeathSound();
}
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 = Utilities.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);
}
}
}
}
}

View File

@ -0,0 +1,62 @@
using System.IO.Compression;
using Bloxstrap.Helpers;
namespace Bloxstrap
{
partial class Bootstrapper
{
private async Task ModifyDeathSound()
{
string fileContentName = "ouch.ogg";
string fileContentLocation = "content\\sounds\\ouch.ogg";
string fileLocation = Path.Combine(VersionFolder, fileContentLocation);
string officialDeathSoundHash = VersionFileManifest[fileContentLocation];
string currentDeathSoundHash = Utilities.CalculateMD5(fileLocation);
if (Program.Settings.UseOldDeathSound && currentDeathSoundHash == officialDeathSoundHash)
{
// let's get the old one!
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
var package = VersionPackageManifest.Find(x => x.Name == "content-sounds.zip");
if (package is null)
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)
return;
if (File.Exists(fileLocation))
File.Delete(fileLocation);
entry.ExtractToFile(fileLocation);
}
}
}
}
}

View File

@ -0,0 +1,178 @@
using System.Diagnostics;
using Bloxstrap.Enums;
using Bloxstrap.Dialogs.BootstrapperStyles;
using Bloxstrap.Helpers;
using Bloxstrap.Helpers.RSMM;
namespace Bloxstrap
{
public partial class Bootstrapper
{
public Bootstrapper()
{
if (Program.BaseDirectory is null)
return;
FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid);
DownloadsFolder = Path.Combine(Program.BaseDirectory, "Downloads");
Client.Timeout = TimeSpan.FromMinutes(10);
}
public void Initialize(BootstrapperStyle bootstrapperStyle, string? launchCommandLine = null)
{
LaunchCommandLine = launchCommandLine;
switch (bootstrapperStyle)
{
case BootstrapperStyle.VistaDialog:
new VistaDialog(this);
break;
case BootstrapperStyle.LegacyDialog:
Application.Run(new LegacyDialog(this));
break;
case BootstrapperStyle.ProgressDialog:
Application.Run(new ProgressDialog(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();
Program.Exit();
}
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;
using (SystemEvent startEvent = new(startEventName))
{
Process gameClient = Process.Start(Path.Combine(VersionFolder, "RobloxPlayerBeta.exe"), LaunchCommandLine);
bool startEventFired = await startEvent.WaitForEvent();
startEvent.Close();
if (!startEventFired)
return;
// event fired, wait for 6 seconds then close
await Task.Delay(6000);
// now we move onto handling rich presence
// except beta app launch since we have to rely strictly on website launch
if (!Program.Settings.UseDiscordRichPresence || LaunchCommandLine.Contains("--app"))
return;
// probably not the most ideal way to do this
string? placeId = Utilities.GetKeyValue(LaunchCommandLine, "placeId=", '&');
if (placeId is null)
return;
// keep bloxstrap open to handle rich presence
using (DiscordRichPresence richPresence = new())
{
bool presenceSet = await richPresence.SetPresence(placeId);
if (!presenceSet)
return;
CloseDialog();
await gameClient.WaitForExitAsync();
}
}
}
public void CancelButtonClicked()
{
if (Program.BaseDirectory is null)
return;
CancelFired = true;
try
{
if (Program.IsFirstRun)
Directory.Delete(Program.BaseDirectory, true);
else if (Directory.Exists(VersionFolder))
Directory.Delete(VersionFolder, true);
}
catch (Exception) { }
Program.Exit();
}
private void ShowSuccess(string message)
{
ShowSuccessEvent.Invoke(this, new ChangeEventArgs<string>(message));
}
private void PromptShutdown()
{
PromptShutdownEvent.Invoke(this, new EventArgs());
}
private void CloseDialog()
{
CloseDialogEvent.Invoke(this, new EventArgs());
}
}
}

View File

@ -1,6 +1,6 @@
namespace Bloxstrap.Dialogs.BootstrapperStyles
{
partial class LegacyDialogStyle
partial class LegacyDialog
{
/// <summary>
/// Required designer variable.
@ -31,7 +31,7 @@
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();
this.ButtonCancel = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit();
this.SuspendLayout();
//
@ -62,25 +62,25 @@
this.IconBox.TabIndex = 2;
this.IconBox.TabStop = false;
//
// CancelButton
// ButtonCancel
//
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);
this.ButtonCancel.Enabled = false;
this.ButtonCancel.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.ButtonCancel.Location = new System.Drawing.Point(271, 83);
this.ButtonCancel.Name = "ButtonCancel";
this.ButtonCancel.Size = new System.Drawing.Size(75, 23);
this.ButtonCancel.TabIndex = 3;
this.ButtonCancel.Text = "Cancel";
this.ButtonCancel.UseVisualStyleBackColor = true;
this.ButtonCancel.Visible = false;
this.ButtonCancel.Click += new System.EventHandler(this.ButtonCancel_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.ButtonCancel);
this.Controls.Add(this.IconBox);
this.Controls.Add(this.ProgressBar);
this.Controls.Add(this.Message);
@ -103,6 +103,6 @@
private Label Message;
private ProgressBar ProgressBar;
private PictureBox IconBox;
private Button CancelButton;
private Button ButtonCancel;
}
}

View File

@ -11,27 +11,17 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
// 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
public partial class LegacyDialog : Form
{
private Bootstrapper? Bootstrapper;
private readonly Bootstrapper? Bootstrapper;
public LegacyDialogStyle(Bootstrapper? bootstrapper = null)
public LegacyDialog(Bootstrapper? bootstrapper = null)
{
InitializeComponent();
if (bootstrapper is not null)
{
Bootstrapper = bootstrapper;
Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown);
Bootstrapper.ShowSuccessEvent += new ChangeEventHandler<string>(ShowSuccess);
Bootstrapper.MessageChanged += new ChangeEventHandler<string>(MessageChanged);
Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler<int>(ProgressBarValueChanged);
Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler<ProgressBarStyle>(ProgressBarStyleChanged);
Bootstrapper.CancelEnabledChanged += new ChangeEventHandler<bool>(CancelEnabledChanged);
}
Bootstrapper = bootstrapper;
Icon icon = IconManager.GetIconResource();
this.Text = Program.ProjectName;
this.Icon = icon;
this.IconBox.Image = icon.ToBitmap();
@ -39,17 +29,28 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
if (Bootstrapper is null)
{
this.Message.Text = "Click the Cancel button to return to preferences";
this.CancelButton.Enabled = true;
this.CancelButton.Visible = true;
this.ButtonCancel.Enabled = true;
this.ButtonCancel.Visible = true;
}
else
{
Bootstrapper.CloseDialogEvent += new EventHandler(CloseDialog);
Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown);
Bootstrapper.ShowSuccessEvent += new ChangeEventHandler<string>(ShowSuccess);
Bootstrapper.MessageChanged += new ChangeEventHandler<string>(MessageChanged);
Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler<int>(ProgressBarValueChanged);
Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler<ProgressBarStyle>(ProgressBarStyleChanged);
Bootstrapper.CancelEnabledChanged += new ChangeEventHandler<bool>(CancelEnabledChanged);
Task.Run(() => RunBootstrapper());
}
}
public async void RunBootstrapper()
{
if (Bootstrapper is null)
return;
try
{
await Bootstrapper.Run();
@ -84,6 +85,11 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
);
}
private void CloseDialog(object? sender, EventArgs e)
{
this.Close();
}
private void PromptShutdown(object? sender, EventArgs e)
{
DialogResult result = MessageBox.Show(
@ -138,19 +144,19 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
private void CancelEnabledChanged(object sender, ChangeEventArgs<bool> e)
{
if (this.CancelButton.InvokeRequired)
if (this.ButtonCancel.InvokeRequired)
{
ChangeEventHandler<bool> handler = new(CancelEnabledChanged);
this.CancelButton.Invoke(handler, sender, e);
this.ButtonCancel.Invoke(handler, sender, e);
}
else
{
this.CancelButton.Enabled = e.Value;
this.CancelButton.Visible = e.Value;
this.ButtonCancel.Enabled = e.Value;
this.ButtonCancel.Visible = e.Value;
}
}
private void CancelButton_Click(object sender, EventArgs e)
private void ButtonCancel_Click(object sender, EventArgs e)
{
if (Bootstrapper is null)
this.Close();

View File

@ -1,6 +1,6 @@
namespace Bloxstrap.Dialogs.BootstrapperStyles
{
partial class ProgressDialogStyle
partial class ProgressDialog
{
/// <summary>
/// Required designer variable.
@ -31,10 +31,10 @@
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.ButtonCancel = new System.Windows.Forms.PictureBox();
this.panel1 = new System.Windows.Forms.Panel();
((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.CancelButton)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.ButtonCancel)).BeginInit();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
@ -69,26 +69,26 @@
this.IconBox.TabIndex = 2;
this.IconBox.TabStop = false;
//
// CancelButton
// ButtonCancel
//
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);
this.ButtonCancel.Enabled = false;
this.ButtonCancel.Image = global::Bloxstrap.Properties.Resources.CancelButton;
this.ButtonCancel.Location = new System.Drawing.Point(194, 264);
this.ButtonCancel.Name = "ButtonCancel";
this.ButtonCancel.Size = new System.Drawing.Size(130, 44);
this.ButtonCancel.TabIndex = 3;
this.ButtonCancel.TabStop = false;
this.ButtonCancel.Visible = false;
this.ButtonCancel.Click += new System.EventHandler(this.ButtonCancel_Click);
this.ButtonCancel.MouseEnter += new System.EventHandler(this.ButtonCancel_MouseEnter);
this.ButtonCancel.MouseLeave += new System.EventHandler(this.ButtonCancel_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.ButtonCancel);
this.panel1.Controls.Add(this.ProgressBar);
this.panel1.Location = new System.Drawing.Point(1, 1);
this.panel1.Name = "panel1";
@ -109,7 +109,7 @@
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "ProgressDialogStyle";
((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.CancelButton)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.ButtonCancel)).EndInit();
this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
@ -120,7 +120,7 @@
private ProgressBar ProgressBar;
private Label Message;
private PictureBox IconBox;
private PictureBox CancelButton;
private PictureBox ButtonCancel;
private Panel panel1;
}
}

View File

@ -1,15 +1,17 @@
using Bloxstrap.Helpers;
using System.Diagnostics;
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
public partial class ProgressDialog : Form
{
private Bootstrapper? Bootstrapper;
private readonly Bootstrapper? Bootstrapper;
public ProgressDialogStyle(Bootstrapper? bootstrapper = null)
public ProgressDialog(Bootstrapper? bootstrapper = null)
{
InitializeComponent();
@ -22,11 +24,12 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
if (Bootstrapper is null)
{
this.Message.Text = "Click the Cancel button to return to preferences";
this.CancelButton.Enabled = true;
this.CancelButton.Visible = true;
this.ButtonCancel.Enabled = true;
this.ButtonCancel.Visible = true;
}
else
{
Bootstrapper.CloseDialogEvent += new EventHandler(CloseDialog);
Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown);
Bootstrapper.ShowSuccessEvent += new ChangeEventHandler<string>(ShowSuccess);
Bootstrapper.MessageChanged += new ChangeEventHandler<string>(MessageChanged);
@ -40,6 +43,9 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
public async void RunBootstrapper()
{
if (Bootstrapper is null)
return;
try
{
await Bootstrapper.Run();
@ -74,6 +80,11 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
);
}
private void CloseDialog(object? sender, EventArgs e)
{
this.Hide();
}
private void PromptShutdown(object? sender, EventArgs e)
{
DialogResult result = MessageBox.Show(
@ -128,19 +139,19 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
private void CancelEnabledChanged(object sender, ChangeEventArgs<bool> e)
{
if (this.CancelButton.InvokeRequired)
if (this.ButtonCancel.InvokeRequired)
{
ChangeEventHandler<bool> handler = new(CancelEnabledChanged);
this.CancelButton.Invoke(handler, sender, e);
this.ButtonCancel.Invoke(handler, sender, e);
}
else
{
this.CancelButton.Enabled = e.Value;
this.CancelButton.Visible = e.Value;
this.ButtonCancel.Enabled = e.Value;
this.ButtonCancel.Visible = e.Value;
}
}
private void CancelButton_Click(object sender, EventArgs e)
private void ButtonCancel_Click(object sender, EventArgs e)
{
if (Bootstrapper is null)
this.Close();
@ -148,14 +159,14 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
Task.Run(() => Bootstrapper.CancelButtonClicked());
}
private void CancelButton_MouseEnter(object sender, EventArgs e)
private void ButtonCancel_MouseEnter(object sender, EventArgs e)
{
this.CancelButton.Image = Properties.Resources.CancelButtonHover;
this.ButtonCancel.Image = Properties.Resources.CancelButtonHover;
}
private void CancelButton_MouseLeave(object sender, EventArgs e)
private void ButtonCancel_MouseLeave(object sender, EventArgs e)
{
this.CancelButton.Image = Properties.Resources.CancelButton;
this.ButtonCancel.Image = Properties.Resources.CancelButton;
}
}
}

View File

@ -5,10 +5,6 @@ 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
@ -17,12 +13,12 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles
// for now, just stick to legacydialog and progressdialog
public class TaskDialogStyle
public class VistaDialog
{
private Bootstrapper Bootstrapper;
private readonly Bootstrapper Bootstrapper;
private TaskDialogPage Dialog;
public TaskDialogStyle(Bootstrapper bootstrapper)
public VistaDialog(Bootstrapper bootstrapper)
{
Bootstrapper = bootstrapper;
Bootstrapper.ShowSuccessEvent += new ChangeEventHandler<string>(ShowSuccess);

View File

@ -28,9 +28,12 @@
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.label1 = new System.Windows.Forms.Label();
this.Tabs = new System.Windows.Forms.TabControl();
this.DialogTab = new System.Windows.Forms.TabPage();
this.groupBox5 = new System.Windows.Forms.GroupBox();
this.ToggleDiscordRichPresence = new System.Windows.Forms.CheckBox();
this.groupBox3 = new System.Windows.Forms.GroupBox();
this.IconPreview = new System.Windows.Forms.PictureBox();
this.IconSelection = new System.Windows.Forms.ListBox();
@ -38,23 +41,24 @@
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.ToggleDeathSound = new System.Windows.Forms.CheckBox();
this.GroupBoxInstallLocation = 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.InfoTooltip = new System.Windows.Forms.ToolTip(this.components);
this.Tabs.SuspendLayout();
this.DialogTab.SuspendLayout();
this.groupBox5.SuspendLayout();
this.groupBox3.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.IconPreview)).BeginInit();
this.groupBox2.SuspendLayout();
this.InstallationTab.SuspendLayout();
this.groupBox4.SuspendLayout();
this.groupBox1.SuspendLayout();
this.GroupBoxInstallLocation.SuspendLayout();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
@ -66,7 +70,7 @@
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(237, 23);
this.label1.TabIndex = 1;
this.label1.Text = "Configure Preferences";
this.label1.Text = "Configure Bloxstrap";
//
// Tabs
//
@ -75,21 +79,45 @@
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.Size = new System.Drawing.Size(442, 226);
this.Tabs.TabIndex = 2;
//
// DialogTab
//
this.DialogTab.Controls.Add(this.groupBox5);
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.Size = new System.Drawing.Size(434, 198);
this.DialogTab.TabIndex = 0;
this.DialogTab.Text = "Bootstrapper";
this.DialogTab.UseVisualStyleBackColor = true;
//
// groupBox5
//
this.groupBox5.Controls.Add(this.ToggleDiscordRichPresence);
this.groupBox5.Location = new System.Drawing.Point(5, 146);
this.groupBox5.Name = "groupBox5";
this.groupBox5.Size = new System.Drawing.Size(422, 46);
this.groupBox5.TabIndex = 7;
this.groupBox5.TabStop = false;
this.groupBox5.Text = "Launch";
//
// ToggleDiscordRichPresence
//
this.ToggleDiscordRichPresence.AutoSize = true;
this.ToggleDiscordRichPresence.Checked = true;
this.ToggleDiscordRichPresence.CheckState = System.Windows.Forms.CheckState.Checked;
this.ToggleDiscordRichPresence.Location = new System.Drawing.Point(9, 19);
this.ToggleDiscordRichPresence.Name = "ToggleDiscordRichPresence";
this.ToggleDiscordRichPresence.Size = new System.Drawing.Size(274, 19);
this.ToggleDiscordRichPresence.TabIndex = 0;
this.ToggleDiscordRichPresence.Text = "Show game activity with Discord Rich Presence";
this.ToggleDiscordRichPresence.UseVisualStyleBackColor = true;
this.ToggleDiscordRichPresence.CheckedChanged += new System.EventHandler(this.ToggleDiscordRichPresence_CheckedChanged);
//
// groupBox3
//
this.groupBox3.Controls.Add(this.IconPreview);
@ -144,49 +172,49 @@
// InstallationTab
//
this.InstallationTab.Controls.Add(this.groupBox4);
this.InstallationTab.Controls.Add(this.groupBox1);
this.InstallationTab.Controls.Add(this.GroupBoxInstallLocation);
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.Size = new System.Drawing.Size(434, 198);
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.Controls.Add(this.ToggleDeathSound);
this.groupBox4.Location = new System.Drawing.Point(5, 60);
this.groupBox4.Name = "groupBox4";
this.groupBox4.Size = new System.Drawing.Size(422, 84);
this.groupBox4.Size = new System.Drawing.Size(422, 46);
this.groupBox4.TabIndex = 2;
this.groupBox4.TabStop = false;
this.groupBox4.Text = "Modifications";
//
// ModifyDeathSoundToggle
// ToggleDeathSound
//
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);
this.ToggleDeathSound.AutoSize = true;
this.ToggleDeathSound.Checked = true;
this.ToggleDeathSound.CheckState = System.Windows.Forms.CheckState.Checked;
this.ToggleDeathSound.Location = new System.Drawing.Point(9, 19);
this.ToggleDeathSound.Margin = new System.Windows.Forms.Padding(2);
this.ToggleDeathSound.Name = "ToggleDeathSound";
this.ToggleDeathSound.Size = new System.Drawing.Size(134, 19);
this.ToggleDeathSound.TabIndex = 1;
this.ToggleDeathSound.Text = "Use old death sound";
this.ToggleDeathSound.UseVisualStyleBackColor = true;
this.ToggleDeathSound.CheckedChanged += new System.EventHandler(this.ToggleDeathSound_CheckedChanged);
//
// groupBox1
// GroupBoxInstallLocation
//
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";
this.GroupBoxInstallLocation.Controls.Add(this.InstallLocationBrowseButton);
this.GroupBoxInstallLocation.Controls.Add(this.InstallLocation);
this.GroupBoxInstallLocation.Location = new System.Drawing.Point(5, 3);
this.GroupBoxInstallLocation.Name = "GroupBoxInstallLocation";
this.GroupBoxInstallLocation.Size = new System.Drawing.Size(422, 54);
this.GroupBoxInstallLocation.TabIndex = 0;
this.GroupBoxInstallLocation.TabStop = false;
this.GroupBoxInstallLocation.Text = "Install Location";
//
// InstallLocationBrowseButton
//
@ -225,24 +253,13 @@
| 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.Location = new System.Drawing.Point(-1, 277);
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);
@ -254,12 +271,18 @@
this.PreviewButton.UseVisualStyleBackColor = true;
this.PreviewButton.Click += new System.EventHandler(this.PreviewButton_Click);
//
// InfoTooltip
//
this.InfoTooltip.ShowAlways = true;
this.InfoTooltip.ToolTipIcon = System.Windows.Forms.ToolTipIcon.Info;
this.InfoTooltip.ToolTipTitle = "Information";
//
// 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.ClientSize = new System.Drawing.Size(464, 318);
this.Controls.Add(this.panel1);
this.Controls.Add(this.Tabs);
this.Controls.Add(this.label1);
@ -271,16 +294,17 @@
this.Text = "Preferences";
this.Tabs.ResumeLayout(false);
this.DialogTab.ResumeLayout(false);
this.groupBox5.ResumeLayout(false);
this.groupBox5.PerformLayout();
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.GroupBoxInstallLocation.ResumeLayout(false);
this.GroupBoxInstallLocation.PerformLayout();
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
this.ResumeLayout(false);
}
@ -294,7 +318,7 @@
private Button SaveButton;
private Panel panel1;
private ListBox StyleSelection;
private GroupBox groupBox1;
private GroupBox GroupBoxInstallLocation;
private Button InstallLocationBrowseButton;
private TextBox InstallLocation;
private FolderBrowserDialog InstallLocationBrowseDialog;
@ -303,8 +327,10 @@
private PictureBox IconPreview;
private ListBox IconSelection;
private Button PreviewButton;
private Label label2;
private CheckBox ModifyDeathSoundToggle;
private CheckBox ToggleDeathSound;
private GroupBox groupBox4;
private GroupBox groupBox5;
private CheckBox ToggleDiscordRichPresence;
private ToolTip InfoTooltip;
}
}

View File

@ -25,24 +25,10 @@ namespace Bloxstrap.Dialogs
{ "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 bool _useDiscordRichPresence = true;
private bool _useOldDeathSound = true;
private BootstrapperStyle SelectedStyle
{
@ -77,16 +63,48 @@ namespace Bloxstrap.Dialogs
}
}
private bool UseDiscordRichPresence
{
get => _useDiscordRichPresence;
set
{
if (_useDiscordRichPresence == value)
return;
_useDiscordRichPresence = value;
this.ToggleDiscordRichPresence.Checked = value;
}
}
private bool UseOldDeathSound
{
get => _useOldDeathSound;
set
{
if (_useOldDeathSound == value)
return;
_useOldDeathSound = value;
this.ToggleDeathSound.Checked = value;
}
}
public Preferences()
{
InitializeComponent();
Program.SettingsManager.ShouldSave = false;
this.Icon = Properties.Resources.IconBloxstrap_ico;
this.Text = Program.ProjectName;
if (Program.IsFirstRun)
{
this.SaveButton.Text = "Continue";
this.SaveButton.Text = "Install";
this.InstallLocation.Text = Path.Combine(Program.LocalAppData, Program.ProjectName);
}
else
@ -104,14 +122,15 @@ namespace Bloxstrap.Dialogs
this.IconSelection.Items.Add(icon.Key);
}
UseOldDeathSound = Program.Settings.UseOldDeathSound;
this.InfoTooltip.SetToolTip(this.StyleSelection, "Choose how the bootstrapper dialog should look.");
this.InfoTooltip.SetToolTip(this.IconSelection, "Choose what icon the bootstrapper should use.");
this.InfoTooltip.SetToolTip(this.GroupBoxInstallLocation, "Choose where Bloxstrap should install to.\nThis is useful if you typically install all your games to a separate storage drive.");
this.InfoTooltip.SetToolTip(this.ToggleDiscordRichPresence, "Choose whether to show what game you're playing on your Discord profile.\nThis will ONLY work when you launch a game from the website, and is not supported in the Beta App.");
SelectedStyle = Program.Settings.BootstrapperStyle;
SelectedIcon = Program.Settings.BootstrapperIcon;
}
private void ShowDialog(MessageBoxIcon icon, string message)
{
MessageBox.Show(message, Program.ProjectName, MessageBoxButtons.OK, icon);
UseDiscordRichPresence = Program.Settings.UseDiscordRichPresence;
UseOldDeathSound = Program.Settings.UseOldDeathSound;
}
private void InstallLocationBrowseButton_Click(object sender, EventArgs e)
@ -140,7 +159,7 @@ namespace Bloxstrap.Dialogs
if (String.IsNullOrEmpty(installLocation))
{
ShowDialog(MessageBoxIcon.Error, "You must set an install location");
Program.ShowMessageBox(MessageBoxIcon.Error, "You must set an install location");
return;
}
@ -163,12 +182,12 @@ namespace Bloxstrap.Dialogs
}
catch (UnauthorizedAccessException)
{
ShowDialog(MessageBoxIcon.Error, $"{Program.ProjectName} does not have write access to the install location you selected. Please choose another install location.");
Program.ShowMessageBox(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);
Program.ShowMessageBox(MessageBoxIcon.Error, ex.Message);
return;
}
@ -179,7 +198,7 @@ namespace Bloxstrap.Dialogs
}
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.ShowMessageBox(MessageBoxIcon.Information, $"{Program.ProjectName} will install to the new location you've set the next time it runs.");
Program.Settings.VersionGuid = "";
@ -196,9 +215,13 @@ namespace Bloxstrap.Dialogs
File.Copy(Path.Combine(Program.BaseDirectory, "Settings.json"), Path.Combine(installLocation, "Settings.json"));
}
Program.Settings.UseOldDeathSound = UseOldDeathSound;
if (!Program.IsFirstRun)
Program.SettingsManager.ShouldSave = true;
Program.Settings.BootstrapperStyle = SelectedStyle;
Program.Settings.BootstrapperIcon = SelectedIcon;
Program.Settings.UseDiscordRichPresence = UseDiscordRichPresence;
Program.Settings.UseOldDeathSound = UseOldDeathSound;
this.Close();
}
@ -214,11 +237,11 @@ namespace Bloxstrap.Dialogs
switch (SelectedStyle)
{
case BootstrapperStyle.LegacyDialog:
new LegacyDialogStyle().ShowDialog();
new LegacyDialog().ShowDialog();
break;
case BootstrapperStyle.ProgressDialog:
new ProgressDialogStyle().ShowDialog();
new ProgressDialog().ShowDialog();
break;
}
@ -227,9 +250,14 @@ namespace Bloxstrap.Dialogs
this.Visible = true;
}
private void ModifyDeathSoundToggle_CheckedChanged(object sender, EventArgs e)
private void ToggleDiscordRichPresence_CheckedChanged(object sender, EventArgs e)
{
UseOldDeathSound = this.ModifyDeathSoundToggle.Checked;
UseDiscordRichPresence = this.ToggleDiscordRichPresence.Checked;
}
private void ToggleDeathSound_CheckedChanged(object sender, EventArgs e)
{
UseOldDeathSound = this.ToggleDeathSound.Checked;
}
}
}

View File

@ -60,4 +60,7 @@
<metadata name="InstallLocationBrowseDialog.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="InfoTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>222, 17</value>
</metadata>
</root>

View File

@ -2,7 +2,7 @@
{
public enum BootstrapperStyle
{
TaskDialog,
VistaDialog,
LegacyDialog,
ProgressDialog
}

View File

@ -0,0 +1,73 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using DiscordRPC;
namespace Bloxstrap.Helpers
{
internal class DiscordRichPresence : IDisposable
{
readonly DiscordRpcClient RichPresence = new("1005469189907173486");
public async Task<bool> SetPresence(string placeId)
{
string placeName;
string placeThumbnail;
string creatorName;
// null checking could probably be a lot more concrete here
using (HttpClient client = new())
{
JObject placeInfo = await Utilities.GetJson($"https://economy.roblox.com/v2/assets/{placeId}/details");
placeName = placeInfo["Name"].Value<string>();
creatorName = placeInfo["Creator"]["Name"].Value<string>();
JObject thumbnailInfo = await Utilities.GetJson($"https://thumbnails.roblox.com/v1/places/gameicons?placeIds={placeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false");
if (thumbnailInfo["data"] is null)
return false;
placeThumbnail = thumbnailInfo["data"][0]["imageUrl"].Value<string>();
}
RichPresence.Initialize();
RichPresence.SetPresence(new RichPresence()
{
Details = placeName,
State = $"by {creatorName}",
Timestamps = new Timestamps() { Start = DateTime.UtcNow },
Assets = new Assets()
{
LargeImageKey = placeThumbnail,
LargeImageText = placeName,
SmallImageKey = "bloxstrap",
SmallImageText = "Rich Presence provided by Bloxstrap"
},
Buttons = new DiscordRPC.Button[]
{
new DiscordRPC.Button()
{
Label = "Play",
Url = $"https://www.roblox.com/games/start?placeId={placeId}&launchData=%7B%7D"
},
new DiscordRPC.Button()
{
Label = "View Details",
Url = $"https://www.roblox.com/games/{placeId}"
}
}
});
return true;
}
public void Dispose()
{
RichPresence.Dispose();
}
}
}

View File

@ -0,0 +1,76 @@
using System.Diagnostics;
using Newtonsoft.Json.Linq;
namespace Bloxstrap.Helpers
{
public class UpdateChecker
{
public static void CheckInstalledVersion()
{
if (Environment.ProcessPath is null || !File.Exists(Program.FilePath))
return;
// if downloaded version doesn't match, replace installed version with downloaded version
FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath);
FileVersionInfo installedVersionInfo = FileVersionInfo.GetVersionInfo(Program.FilePath);
if (installedVersionInfo != currentVersionInfo)
{
DialogResult result = MessageBox.Show(
$"The version of {Program.ProjectName} you've launched is newer than the version you currently have installed.\nWould you like to update your installed version of {Program.ProjectName}?",
Program.ProjectName,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (result == DialogResult.Yes)
{
File.Delete(Program.FilePath);
File.Copy(Environment.ProcessPath, Program.FilePath);
}
}
}
public static async Task Check()
{
if (Environment.ProcessPath is null)
return;
FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath);
string currentVersion = $"Bloxstrap v{currentVersionInfo.ProductVersion}";
string latestVersion;
string releaseNotes;
// get the latest version according to the latest github release info
// it should contain the latest product version, which we can check against
try
{
JObject releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{Program.ProjectRepository}/releases/latest");
latestVersion = releaseInfo["name"].Value<string>();
releaseNotes = releaseInfo["body"].Value<string>();
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to fetch latest version info! ({ex.Message})");
return;
}
if (currentVersion != latestVersion)
{
DialogResult result = MessageBox.Show(
$"A new version of {Program.ProjectName} is available\n\nRelease notes:\n{releaseNotes}\n\nDo you want to download {latestVersion}?",
Program.ProjectName,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (result == DialogResult.Yes)
{
Process.Start(new ProcessStartInfo { FileName = $"https://github.com/{Program.ProjectRepository}/releases/latest", UseShellExecute = true });
Program.Exit();
}
}
}
}
}

View File

@ -0,0 +1,48 @@
using System.Security.Cryptography;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Bloxstrap.Helpers
{
public class Utilities
{
public static async Task<JObject> GetJson(string url)
{
using (HttpClient client = new())
{
client.DefaultRequestHeaders.Add("User-Agent", Program.ProjectRepository);
string jsonString = await client.GetStringAsync(url);
return (JObject)JsonConvert.DeserializeObject(jsonString);
}
}
public 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();
}
}
}
// quick and hacky way of getting a value from any key/value pair formatted list
// (command line args, uri params, etc)
public static string? GetKeyValue(string subject, string key, char delimiter)
{
if (subject.LastIndexOf(key) == -1)
return null;
string substr = subject.Substring(subject.LastIndexOf(key) + key.Length);
if (substr.IndexOf(delimiter) == -1)
return null;
return substr.Split(delimiter)[0];
}
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.Win32;
using System.Diagnostics;
using Microsoft.Win32;
using Bloxstrap.Helpers;
namespace Bloxstrap
@ -19,11 +19,17 @@ namespace Bloxstrap
public static string? BaseDirectory;
public static string LocalAppData { get; private set; }
public static string FilePath { get; private set; }
public static string StartMenuDirectory { get; private set; }
public static bool IsFirstRun { get; private set; } = false;
public static SettingsFormat Settings;
public static SettingsManager SettingsManager = new();
public static void ShowMessageBox(MessageBoxIcon icon, string message)
{
MessageBox.Show(message, Program.ProjectName, MessageBoxButtons.OK, icon);
}
public static void Exit()
{
SettingsManager.Save();
@ -40,16 +46,17 @@ namespace Bloxstrap
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
// ensure only one is running
Process[] processes = Process.GetProcessesByName(ProjectName);
if (processes.Length > 1)
if (Process.GetProcessesByName(ProjectName).Length > 1)
{
ShowMessageBox(MessageBoxIcon.Error, $"{ProjectName} is already running. Please close any currently open {ProjectName} window.\nIf you have Discord Rich Presence enabled, then close Roblox if it's running.");
return;
}
// Task.Run(() => Updater.CheckForUpdates()).Wait();
// return;
UpdateChecker.Check().Wait();
LocalAppData = Environment.GetEnvironmentVariable("localappdata");
LocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// check if installed
RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}");
if (registryKey is null)
@ -64,18 +71,20 @@ namespace Bloxstrap
registryKey.Close();
}
// selection dialog was closed
// preferences dialog was closed, and so base directory was never set
// (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");
StartMenuDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProjectName);
// 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)
{
UpdateChecker.CheckInstalledVersion();
Settings = SettingsManager.Settings;
SettingsManager.ShouldSave = true;
}
@ -107,7 +116,7 @@ namespace Bloxstrap
}
if (!String.IsNullOrEmpty(commandLine))
new Bootstrapper(Settings.BootstrapperStyle, commandLine);
new Bootstrapper().Initialize(Settings.BootstrapperStyle, commandLine);
SettingsManager.Save();
}

View File

@ -7,9 +7,11 @@ 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 bool UseDiscordRichPresence { get; set; } = true;
public bool UseOldDeathSound { get; set; } = true;
}
public class SettingsManager

1
DiscordRPC Submodule

@ -0,0 +1 @@
Subproject commit a9fcc8d1e85738bc6493474a62a961842fa8dbc3