This commit is contained in:
pizzaboxer 2022-12-26 23:42:58 +00:00
commit 3a52aeeb67
24 changed files with 272 additions and 504 deletions

14
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "daily"

66
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: CI
on:
push:
tags:
- 'v*'
branches:
- main
pull_request:
branches: [ main ]
jobs:
build:
strategy:
matrix:
configuration: [Debug, Release]
platform: [x64, x86]
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Publish
run: dotnet publish -p:PublishSingleFile=true -r win-${{ matrix.platform }} -c ${{ matrix.configuration }} --self-contained false .\Bloxstrap\Bloxstrap.csproj
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: Bloxstrap (${{ matrix.configuration }}, ${{ matrix.platform }})
path: |
.\Bloxstrap\bin\${{ matrix.configuration }}\net6.0-windows\win-${{ matrix.platform }}\publish\*
release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download x64 release artifact
uses: actions/download-artifact@v3
with:
name: Bloxstrap (Release, x64)
path: x64
- name: Download x86 release artifact
uses: actions/download-artifact@v3
with:
name: Bloxstrap (Release, x86)
path: x86
- name: Rename binaries
run: |
mv x64/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}-x64.exe
mv x86/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}-x86.exe
- name: Release
uses: softprops/action-gh-release@v1
with:
draft: true
files: |
Bloxstrap-${{ github.ref_name }}-x64.exe
Bloxstrap-${{ github.ref_name }}-x86.exe
name: Bloxstrap ${{ github.ref_name }}

View File

@ -10,8 +10,8 @@
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<Platforms>AnyCPU;x86</Platforms> <Platforms>AnyCPU;x86</Platforms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon> <ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>1.5.3</Version> <Version>1.5.4</Version>
<FileVersion>1.5.3.0</FileVersion> <FileVersion>1.5.4.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -10,12 +10,21 @@ using Bloxstrap.Dialogs.BootstrapperDialogs;
using Bloxstrap.Helpers; using Bloxstrap.Helpers;
using Bloxstrap.Helpers.Integrations; using Bloxstrap.Helpers.Integrations;
using Bloxstrap.Helpers.RSMM; using Bloxstrap.Helpers.RSMM;
using Bloxstrap.Models;
using System.Net;
namespace Bloxstrap namespace Bloxstrap
{ {
public partial class Bootstrapper public partial class Bootstrapper
{ {
#region Properties #region Properties
// https://learn.microsoft.com/en-us/windows/win32/msi/error-codes
public const int ERROR_SUCCESS = 0;
public const int ERROR_INSTALL_USEREXIT = 1602;
public const int ERROR_INSTALL_FAILURE = 1603;
public const int ERROR_PRODUCT_UNINSTALLED = 1614;
// in case a new package is added, you can find the corresponding directory // in case a new package is added, you can find the corresponding directory
// by opening the stock bootstrapper in a hex editor // 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? // TODO - there ideally should be a less static way to do this that's not hardcoded?
@ -64,8 +73,6 @@ namespace Bloxstrap
"Any files added here to the root modification directory are ignored, so be sure\n" + "Any files added here to the root modification directory are ignored, so be sure\n" +
"that they're inside a folder."; "that they're inside a folder.";
private static readonly HttpClient Client = new();
private string? LaunchCommandLine; private string? LaunchCommandLine;
private string VersionGuid = null!; private string VersionGuid = null!;
@ -85,13 +92,15 @@ namespace Bloxstrap
{ {
LaunchCommandLine = launchCommandLine; LaunchCommandLine = launchCommandLine;
FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid); FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid);
Client.Timeout = TimeSpan.FromMinutes(10);
} }
// this is called from BootstrapperStyleForm.SetupDialog() // this is called from BootstrapperStyleForm.SetupDialog()
public async Task Run() public async Task Run()
{ {
if (LaunchCommandLine == "-uninstall") if (Program.IsQuiet)
Dialog.CloseDialog();
if (Program.IsUninstall)
{ {
Uninstall(); Uninstall();
return; return;
@ -116,9 +125,12 @@ namespace Bloxstrap
await RbxFpsUnlocker.CheckInstall(); await RbxFpsUnlocker.CheckInstall();
//if (Program.IsFirstRun) Program.SettingsManager.Save();
// Dialog.ShowSuccess($"{Program.ProjectName} has been installed");
//else
if (Program.IsFirstRun && Program.IsNoLaunch)
Dialog.ShowSuccess($"{Program.ProjectName} has successfully installed");
else if (!Program.IsNoLaunch)
await StartRoblox(); await StartRoblox();
Program.Exit(); Program.Exit();
@ -128,7 +140,8 @@ namespace Bloxstrap
{ {
Dialog.Message = "Connecting to Roblox..."; Dialog.Message = "Connecting to Roblox...";
VersionGuid = await Client.GetStringAsync($"{DeployManager.BaseUrl}/version"); ClientVersion clientVersion = await DeployManager.GetLastDeploy(Program.Settings.Channel);
VersionGuid = clientVersion.VersionGuid;
VersionFolder = Path.Combine(Directories.Versions, VersionGuid); VersionFolder = Path.Combine(Directories.Versions, VersionGuid);
VersionPackageManifest = await PackageManifest.Get(VersionGuid); VersionPackageManifest = await PackageManifest.Get(VersionGuid);
} }
@ -236,7 +249,7 @@ namespace Bloxstrap
{ {
if (!Dialog.CancelEnabled) if (!Dialog.CancelEnabled)
{ {
Program.Exit(); Program.Exit(ERROR_INSTALL_USEREXIT);
return; return;
} }
@ -251,7 +264,7 @@ namespace Bloxstrap
} }
catch (Exception) { } catch (Exception) { }
Program.Exit(); Program.Exit(ERROR_INSTALL_USEREXIT);
} }
#endregion #endregion
@ -281,11 +294,16 @@ namespace Bloxstrap
RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}");
uninstallKey.SetValue("DisplayIcon", $"{Directories.App},0"); uninstallKey.SetValue("DisplayIcon", $"{Directories.App},0");
uninstallKey.SetValue("DisplayName", Program.ProjectName); uninstallKey.SetValue("DisplayName", Program.ProjectName);
uninstallKey.SetValue("DisplayVersion", Program.Version);
if (uninstallKey.GetValue("InstallDate") is null)
uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
uninstallKey.SetValue("InstallLocation", Directories.Base); uninstallKey.SetValue("InstallLocation", Directories.Base);
uninstallKey.SetValue("NoRepair", 1); uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", Program.ProjectName); uninstallKey.SetValue("Publisher", "pizzaboxer");
uninstallKey.SetValue("ModifyPath", $"\"{Directories.App}\" -preferences"); uninstallKey.SetValue("ModifyPath", $"\"{Directories.App}\" -preferences");
uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.App}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Directories.App}\" -uninstall"); uninstallKey.SetValue("UninstallString", $"\"{Directories.App}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{Program.ProjectRepository}"); uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{Program.ProjectRepository}");
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Program.ProjectRepository}/releases/latest"); uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Program.ProjectRepository}/releases/latest");
@ -367,6 +385,8 @@ namespace Bloxstrap
catch (Exception) { } catch (Exception) { }
Dialog.ShowSuccess($"{Program.ProjectName} has been uninstalled"); Dialog.ShowSuccess($"{Program.ProjectName} has been uninstalled");
Environment.Exit(ERROR_PRODUCT_UNINSTALLED);
} }
#endregion #endregion
@ -607,7 +627,7 @@ namespace Bloxstrap
{ {
Debug.WriteLine($"Downloading {package.Name}..."); Debug.WriteLine($"Downloading {package.Name}...");
var response = await Client.GetAsync(packageUrl); var response = await Program.HttpClient.GetAsync(packageUrl);
if (CancelFired) if (CancelFired)
return; return;

View File

@ -61,6 +61,9 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
public void SetupDialog() public void SetupDialog()
{ {
if (Program.IsQuiet)
this.Hide();
this.Text = Program.ProjectName; this.Text = Program.ProjectName;
this.Icon = Program.Settings.BootstrapperIcon.GetIcon(); this.Icon = Program.Settings.BootstrapperIcon.GetIcon();
@ -107,7 +110,7 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
public virtual void ShowError(string message) public virtual void ShowError(string message)
{ {
Program.ShowMessageBox($"An error occurred while starting Roblox\n\nDetails: {message}", MessageBoxIcon.Error); Program.ShowMessageBox($"An error occurred while starting Roblox\n\nDetails: {message}", MessageBoxIcon.Error);
Program.Exit(); Program.Exit(Bootstrapper.ERROR_INSTALL_FAILURE);
} }
public virtual void CloseDialog() public virtual void CloseDialog()
@ -127,7 +130,7 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
); );
if (result != DialogResult.OK) if (result != DialogResult.OK)
Environment.Exit(0); Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
} }
public void ButtonCancel_Click(object? sender, EventArgs e) public void ButtonCancel_Click(object? sender, EventArgs e)

View File

@ -41,7 +41,7 @@
this.ShowInTaskbar = false; this.ShowInTaskbar = false;
this.Text = "VistaDialog"; this.Text = "VistaDialog";
this.WindowState = System.Windows.Forms.FormWindowState.Minimized; this.WindowState = System.Windows.Forms.FormWindowState.Minimized;
this.Load += new System.EventHandler(this.TestDialog_Load); this.Load += new System.EventHandler(this.VistaDialog_Load);
this.ResumeLayout(false); this.ResumeLayout(false);
} }

View File

@ -101,7 +101,9 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
successDialog.Buttons[0].Click += (sender, e) => Program.Exit(); successDialog.Buttons[0].Click += (sender, e) => Program.Exit();
if (!Program.IsQuiet)
Dialog.Navigate(successDialog); Dialog.Navigate(successDialog);
Dialog = successDialog; Dialog = successDialog;
} }
} }
@ -129,9 +131,11 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
} }
}; };
errorDialog.Buttons[0].Click += (sender, e) => Program.Exit(); errorDialog.Buttons[0].Click += (sender, e) => Program.Exit(Bootstrapper.ERROR_INSTALL_FAILURE);
if (!Program.IsQuiet)
Dialog.Navigate(errorDialog); Dialog.Navigate(errorDialog);
Dialog = errorDialog; Dialog = errorDialog;
} }
} }
@ -152,8 +156,9 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
} }
private void TestDialog_Load(object sender, EventArgs e) private void VistaDialog_Load(object sender, EventArgs e)
{ {
if (!Program.IsQuiet)
TaskDialog.ShowDialog(Dialog); TaskDialog.ShowDialog(Dialog);
} }
} }

View File

@ -34,7 +34,7 @@
<GroupBox Grid.Column="0" Header="Discord Rich Presence" Margin="10,10,5,5"> <GroupBox Grid.Column="0" Header="Discord Rich Presence" Margin="10,10,5,5">
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">
<CheckBox x:Name="CheckBoxDRPEnabled" Content=" Show game activity" Margin="5" VerticalAlignment="Top" IsChecked="{Binding DRPEnabled, Mode=TwoWay}" /> <CheckBox x:Name="CheckBoxDRPEnabled" Content=" Show game activity" Margin="5" VerticalAlignment="Top" IsChecked="{Binding DRPEnabled, Mode=TwoWay}" />
<CheckBox x:Name="CheckBoxDRPButtons" Content=" Show activity buttons" Margin="5" VerticalAlignment="Top" IsEnabled="{Binding IsChecked, ElementName=CheckBoxDRPEnabled, Mode=OneWay}" IsChecked="{Binding DRPButtons, Mode=TwoWay}" /> <CheckBox x:Name="CheckBoxDRPButtons" Content=" Allow people to join" Margin="5" VerticalAlignment="Top" IsEnabled="{Binding IsChecked, ElementName=CheckBoxDRPEnabled, Mode=OneWay}" IsChecked="{Binding DRPButtons, Mode=TwoWay}" />
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<GroupBox Grid.Column="1" Header="FPS Unlocking" Margin="5,10,10,5"> <GroupBox Grid.Column="1" Header="FPS Unlocking" Margin="5,10,10,5">

View File

@ -349,10 +349,10 @@ namespace Bloxstrap.Dialogs
{ {
ChannelInfo = "Getting latest version info, please wait...\n"; ChannelInfo = "Getting latest version info, please wait...\n";
VersionDeploy info = await DeployManager.GetLastDeploy(channel); ClientVersion info = await DeployManager.GetLastDeploy(channel, true);
string strTimestamp = info.Timestamp.ToString("MM/dd/yyyy h:mm:ss tt", Program.CultureFormat); string? strTimestamp = info.Timestamp?.ToString("MM/dd/yyyy h:mm:ss tt", Program.CultureFormat);
ChannelInfo = $"Version: v{info.FileVersion} ({info.VersionGuid})\nDeployed: {strTimestamp}"; ChannelInfo = $"Version: v{info.Version} ({info.VersionGuid})\nDeployed: {strTimestamp}";
} }
} }
} }

View File

@ -1,6 +1,5 @@
using System.IO; using System.Net.Http;
using System.Net.Http; using System.Text.Json;
using Bloxstrap.Models; using Bloxstrap.Models;
namespace Bloxstrap.Helpers namespace Bloxstrap.Helpers
@ -18,76 +17,30 @@ namespace Bloxstrap.Helpers
public static readonly List<string> ChannelsAbstracted = new List<string>() public static readonly List<string> ChannelsAbstracted = new List<string>()
{ {
"LIVE", "LIVE",
"ZAvatarTeam",
"ZCanary",
//"ZFeatureHarmony", last updated 9/20, shouldn't be here anymore
"ZFlag",
"ZIntegration",
"ZLive",
"ZNext", "ZNext",
//"ZPublic", "ZCanary",
"ZSocialTeam" "ZIntegration"
}; };
// why not? // why not?
public static readonly List<string> ChannelsAll = new List<string>() public static readonly List<string> ChannelsAll = new List<string>()
{ {
"LIVE", "LIVE",
"Ganesh",
"ZAvatarTeam", "ZAvatarTeam",
"ZBugFixBoost-Mutex-Revert", "ZAvatarRelease",
"ZBugFixCLI-54676-Test",
"ZBugFixCLI-55214-Master",
"ZCanary", "ZCanary",
"ZCanary1", "ZCanary1",
"ZCanary2", "ZCanary2",
"ZCanaryApps", "ZCanaryApps",
"ZClientIntegration",
"ZClientWatcher",
"ZFeatureBaseline",
"ZFeatureBoost_Removal_Test_In_Prod",
"ZFeatureFMOD-20115",
"ZFeatureFMOD-Recording-Test",
"ZFeatureHarmony",
"ZFeatureHSR2CDNPlayTest",
"ZFeatureHSR2CDNPlayTest2",
"ZFeatureInstance-Parent-Weak-Ptr",
"ZFeatureInstance-Parent-Weak-Ptr-2",
"ZFeatureLTCG1",
"ZFeatureLuaIInline1",
"ZFeatureQt5.15",
"ZFeatureRail",
"ZFeatureRetchecksV2",
"ZFeatureSubsystemAtomic",
"ZFeatureSubsystemHttpClient",
"ZFeatureTelemLife",
"ZFeatureUse-New-RapidJson-In-Flag-Loading",
"ZFlag", "ZFlag",
"ZIntegration", "ZIntegration",
"ZIntegration1", "ZIntegration1",
"ZLang",
"ZLive", "ZLive",
"ZLive1", "ZLive1",
"ZLoom",
"ZNext", "ZNext",
"ZProject512-Boost-Remove-Mutex-1",
"ZProject516-Boost-Remove-Mutex-Network",
"ZPublic",
"ZQtitanStudio",
"ZQTitanStudioRelease",
"ZReleaseVS2019",
"ZSocialTeam", "ZSocialTeam",
"ZStIntegration",
"ZStudioInt1", "ZStudioInt1",
"ZStudioInt2", "ZStudioInt2"
"ZStudioInt3",
"ZStudioInt4",
"ZStudioInt5",
"ZStudioInt6",
"ZStudioInt7",
"ZStudioInt8",
"ZTesting",
"ZVS2019"
}; };
#endregion #endregion
@ -99,70 +52,37 @@ namespace Bloxstrap.Helpers
return $"{DefaultBaseUrl}/channel/{channel.ToLower()}"; return $"{DefaultBaseUrl}/channel/{channel.ToLower()}";
} }
public static async Task<VersionDeploy> GetLastDeploy(string channel) public static async Task<ClientVersion> GetLastDeploy(string channel, bool timestamp = false)
{ {
string baseUrl = BuildBaseUrl(channel); HttpResponseMessage deployInfoResponse = await Program.HttpClient.GetAsync($"https://clientsettings.roblox.com/v2/client-version/WindowsPlayer/channel/{channel}");
string lastDeploy = "";
using (HttpClient client = new()) if (!deployInfoResponse.IsSuccessStatusCode)
{ {
string deployHistory = await client.GetStringAsync($"{baseUrl}/DeployHistory.txt"); // 400 = Invalid binaryType.
// 404 = Could not find version details for binaryType.
using (StringReader reader = new(deployHistory)) // 500 = Error while fetching version information.
{ // either way, we throw
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
if (line.Contains("WindowsPlayer"))
lastDeploy = line;
}
}
}
if (String.IsNullOrEmpty(lastDeploy))
throw new Exception($"Could not get latest deploy for channel {channel}"); throw new Exception($"Could not get latest deploy for channel {channel}");
}
// here's to hoping roblox doesn't change their deployment entry format string rawJson = await deployInfoResponse.Content.ReadAsStringAsync();
// (last time they did so was may 2021 so we should be fine?) ClientVersion clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawJson)!;
// example entry: 'New WindowsPlayer version-29fb7cdd06e84001 at 8/23/2022 2:07:27 PM, file version: 0, 542, 100, 5420251, git hash: b98d6b2bea36fa2161f48cca979fb620bb0c24fd ...'
// there's a proper way, and then there's the lazy way // for preferences
// this here is the lazy way but it should just work™ if (timestamp)
lastDeploy = lastDeploy[18..]; // 'version-29fb7cdd06e84001 at 8/23/2022 2:07:27 PM, file version: 0, 542, 100, 5420251, git hash: b98d6b2bea36fa2161f48cca979fb620bb0c24fd ...'
string versionGuid = lastDeploy[..lastDeploy.IndexOf(" at")]; // 'version-29fb7cdd06e84001'
lastDeploy = lastDeploy[(versionGuid.Length + 4)..]; // '8/23/2022 2:07:27 PM, file version: 0, 542, 100, 5420251, git hash: b98d6b2bea36fa2161f48cca979fb620bb0c24fd ...'
string strTimestamp = lastDeploy[..lastDeploy.IndexOf(", file")]; // '8/23/2022 2:07:27 PM'
lastDeploy = lastDeploy[(strTimestamp.Length + 16)..]; // '0, 542, 100, 5420251, git hash: b98d6b2bea36fa2161f48cca979fb620bb0c24fd ...'
string fileVersion = "";
if (lastDeploy.Contains("git hash"))
{ {
// ~may 2021 entry: ends like 'file version: 0, 542, 100, 5420251, git hash: b98d6b2bea36fa2161f48cca979fb620bb0c24fd ...' string channelUrl = BuildBaseUrl(channel);
fileVersion = lastDeploy[..lastDeploy.IndexOf(", git")]; // '0, 542, 100, 5420251'
} // get an approximate deploy time from rbxpkgmanifest's last modified date
else HttpResponseMessage pkgResponse = await Program.HttpClient.GetAsync($"{channelUrl}/{clientVersion.VersionGuid}-rbxPkgManifest.txt");
if (pkgResponse.Content.Headers.TryGetValues("last-modified", out var values))
{ {
// pre-may 2021 entry: ends like 'file version: 0, 448, 0, 411122...' string lastModified = values.First();
fileVersion = lastDeploy[..lastDeploy.IndexOf("...")]; // '0, 448, 0, 411122' clientVersion.Timestamp = DateTime.Parse(lastModified);
}
} }
// deployment timestamps are UTC-5 return clientVersion;
strTimestamp += " -05";
DateTime dtTimestamp = DateTime.ParseExact(strTimestamp, "M/d/yyyy h:mm:ss tt zz", Program.CultureFormat).ToLocalTime();
// convert to traditional version format
fileVersion = fileVersion.Replace(" ", "").Replace(',', '.');
return new VersionDeploy
{
VersionGuid = versionGuid,
Timestamp = dtTimestamp,
FileVersion = fileVersion
};
} }
} }
} }

View File

@ -2,7 +2,7 @@
namespace Bloxstrap.Helpers namespace Bloxstrap.Helpers
{ {
internal class Directories class Directories
{ {
public static string Base { get; private set; } = ""; public static string Base { get; private set; } = "";
public static string Downloads { get; private set; } = ""; public static string Downloads { get; private set; } = "";

View File

@ -1,5 +1,4 @@
using System; using System.Diagnostics;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -71,7 +70,6 @@ namespace Bloxstrap.Helpers.Integrations
ActivityMachineAddress = ""; ActivityMachineAddress = "";
await SetPresence(); await SetPresence();
} }
} }
public async void MonitorGameActivity() public async void MonitorGameActivity()
@ -148,24 +146,22 @@ namespace Bloxstrap.Helpers.Integrations
if (thumbnailInfo is not null) if (thumbnailInfo is not null)
placeThumbnail = thumbnailInfo.Data![0].ImageUrl!; placeThumbnail = thumbnailInfo.Data![0].ImageUrl!;
DiscordRPC.Button[]? buttons = null; List<DiscordRPC.Button> buttons = new()
if (!Program.Settings.HideRPCButtons)
{ {
buttons = new DiscordRPC.Button[]
{
new DiscordRPC.Button()
{
Label = "Join",
Url = $"https://www.roblox.com/games/start?placeId={ActivityPlaceId}&gameInstanceId={ActivityJobId}&launchData=%7B%7D"
},
new DiscordRPC.Button() new DiscordRPC.Button()
{ {
Label = "See Details", Label = "See Details",
Url = $"https://www.roblox.com/games/{ActivityPlaceId}" Url = $"https://www.roblox.com/games/{ActivityPlaceId}"
} }
}; };
if (!Program.Settings.HideRPCButtons)
{
buttons.Insert(0, new DiscordRPC.Button()
{
Label = "Join",
Url = $"https://www.roblox.com/games/start?placeId={ActivityPlaceId}&gameInstanceId={ActivityJobId}&launchData=%7B%7D"
});
} }
RichPresence.SetPresence(new RichPresence() RichPresence.SetPresence(new RichPresence()
@ -173,7 +169,7 @@ namespace Bloxstrap.Helpers.Integrations
Details = placeInfo.Name, Details = placeInfo.Name,
State = $"by {placeInfo.Creator.Name}", State = $"by {placeInfo.Creator.Name}",
Timestamps = new Timestamps() { Start = DateTime.UtcNow }, Timestamps = new Timestamps() { Start = DateTime.UtcNow },
Buttons = buttons, Buttons = buttons.ToArray(),
Assets = new Assets() Assets = new Assets()
{ {
LargeImageKey = placeThumbnail, LargeImageKey = placeThumbnail,

View File

@ -70,25 +70,19 @@ namespace Bloxstrap.Helpers.Integrations
return; return;
} }
DateTime lastReleasePublish;
string downloadUrl;
var releaseInfo = await Utilities.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); var releaseInfo = await Utilities.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest");
if (releaseInfo is null || releaseInfo.Assets is null) if (releaseInfo is null || releaseInfo.Assets is null)
return; return;
lastReleasePublish = DateTime.Parse(releaseInfo.CreatedAt); string downloadUrl = releaseInfo.Assets[0].BrowserDownloadUrl;
downloadUrl = releaseInfo.Assets[0].BrowserDownloadUrl;
Directory.CreateDirectory(folderLocation); Directory.CreateDirectory(folderLocation);
if (File.Exists(fileLocation)) if (File.Exists(fileLocation))
{ {
DateTime lastDownload = File.GetCreationTimeUtc(fileLocation);
// no new release published, return // no new release published, return
if (lastDownload > lastReleasePublish) if (Program.Settings.RFUVersion == releaseInfo.TagName)
return; return;
CheckIfRunning(); CheckIfRunning();
@ -97,21 +91,20 @@ namespace Bloxstrap.Helpers.Integrations
Debug.WriteLine("Installing/Updating rbxfpsunlocker..."); Debug.WriteLine("Installing/Updating rbxfpsunlocker...");
using (HttpClient client = new()) byte[] bytes = await Program.HttpClient.GetByteArrayAsync(downloadUrl);
{
byte[] bytes = await client.GetByteArrayAsync(downloadUrl);
using (MemoryStream zipStream = new(bytes)) using (MemoryStream zipStream = new(bytes))
{ {
ZipArchive zip = new(zipStream); ZipArchive zip = new(zipStream);
zip.ExtractToDirectory(folderLocation, true); zip.ExtractToDirectory(folderLocation, true);
} }
}
if (!File.Exists(settingsLocation)) if (!File.Exists(settingsLocation))
{ {
await File.WriteAllTextAsync(settingsLocation, Settings); await File.WriteAllTextAsync(settingsLocation, Settings);
} }
Program.Settings.RFUVersion = releaseInfo.TagName;
} }
} }
} }

View File

@ -75,11 +75,8 @@ namespace Bloxstrap.Helpers.RSMM
string pkgManifestUrl = $"{DeployManager.BaseUrl}/{versionGuid}-rbxPkgManifest.txt"; string pkgManifestUrl = $"{DeployManager.BaseUrl}/{versionGuid}-rbxPkgManifest.txt";
string pkgManifestData; string pkgManifestData;
using (HttpClient http = new()) var getData = Program.HttpClient.GetStringAsync(pkgManifestUrl);
{
var getData = http.GetStringAsync(pkgManifestUrl);
pkgManifestData = await getData.ConfigureAwait(false); pkgManifestData = await getData.ConfigureAwait(false);
}
return new PackageManifest(pkgManifestData); return new PackageManifest(pkgManifestData);
} }

View File

@ -1,288 +0,0 @@
using System.Collections;
using System.IO;
using System.Text;
// taken from MiscUtil: https://github.com/loory/MiscUtil
// the proper usage of MiscUtil nowadays is to *not* use the library and rather copy the code you need so lol
namespace Bloxstrap.Helpers
{
/// <summary>
/// Takes an encoding (defaulting to UTF-8) and a function which produces a seekable stream
/// (or a filename for convenience) and yields lines from the end of the stream backwards.
/// Only single byte encodings, and UTF-8 and Unicode, are supported. The stream
/// returned by the function must be seekable.
/// </summary>
public sealed class ReverseLineReader : IEnumerable<string>
{
/// <summary>
/// Buffer size to use by default. Classes with internal access can specify
/// a different buffer size - this is useful for testing.
/// </summary>
private const int DefaultBufferSize = 4096;
/// <summary>
/// Means of creating a Stream to read from.
/// </summary>
private readonly Func<Stream> streamSource;
/// <summary>
/// Encoding to use when converting bytes to text
/// </summary>
private readonly Encoding encoding;
/// <summary>
/// Size of buffer (in bytes) to read each time we read from the
/// stream. This must be at least as big as the maximum number of
/// bytes for a single character.
/// </summary>
private readonly int bufferSize;
/// <summary>
/// Function which, when given a position within a file and a byte, states whether
/// or not the byte represents the start of a character.
/// </summary>
private Func<long, byte, bool> characterStartDetector;
/// <summary>
/// Creates a LineReader from a stream source. The delegate is only
/// called when the enumerator is fetched. UTF-8 is used to decode
/// the stream into text.
/// </summary>
/// <param name="streamSource">Data source</param>
public ReverseLineReader(Func<Stream> streamSource)
: this(streamSource, Encoding.UTF8)
{
}
/// <summary>
/// Creates a LineReader from a filename. The file is only opened
/// (or even checked for existence) when the enumerator is fetched.
/// UTF8 is used to decode the file into text.
/// </summary>
/// <param name="filename">File to read from</param>
public ReverseLineReader(string filename)
: this(filename, Encoding.UTF8)
{
}
/// <summary>
/// Creates a LineReader from a filename. The file is only opened
/// (or even checked for existence) when the enumerator is fetched.
/// </summary>
/// <param name="filename">File to read from</param>
/// <param name="encoding">Encoding to use to decode the file into text</param>
public ReverseLineReader(string filename, Encoding encoding)
: this(() => File.OpenRead(filename), encoding)
{
}
/// <summary>
/// Creates a LineReader from a stream source. The delegate is only
/// called when the enumerator is fetched.
/// </summary>
/// <param name="streamSource">Data source</param>
/// <param name="encoding">Encoding to use to decode the stream into text</param>
public ReverseLineReader(Func<Stream> streamSource, Encoding encoding)
: this(streamSource, encoding, DefaultBufferSize)
{
}
internal ReverseLineReader(Func<Stream> streamSource, Encoding encoding, int bufferSize)
{
this.streamSource = streamSource;
this.encoding = encoding;
this.bufferSize = bufferSize;
if (encoding.IsSingleByte)
{
// For a single byte encoding, every byte is the start (and end) of a character
characterStartDetector = (pos, data) => true;
}
else if (encoding is UnicodeEncoding)
{
// For UTF-16, even-numbered positions are the start of a character.
// TODO: This assumes no surrogate pairs. More work required
// to handle that.
characterStartDetector = (pos, data) => (pos & 1) == 0;
}
else if (encoding is UTF8Encoding)
{
// For UTF-8, bytes with the top bit clear or the second bit set are the start of a character
// See http://www.cl.cam.ac.uk/~mgk25/unicode.html
characterStartDetector = (pos, data) => (data & 0x80) == 0 || (data & 0x40) != 0;
}
else
{
throw new ArgumentException("Only single byte, UTF-8 and Unicode encodings are permitted");
}
}
/// <summary>
/// Returns the enumerator reading strings backwards. If this method discovers that
/// the returned stream is either unreadable or unseekable, a NotSupportedException is thrown.
/// </summary>
public IEnumerator<string> GetEnumerator()
{
Stream stream = streamSource();
if (!stream.CanSeek)
{
stream.Dispose();
throw new NotSupportedException("Unable to seek within stream");
}
if (!stream.CanRead)
{
stream.Dispose();
throw new NotSupportedException("Unable to read within stream");
}
return GetEnumeratorImpl(stream);
}
private IEnumerator<string> GetEnumeratorImpl(Stream stream)
{
try
{
long position = stream.Length;
if (encoding is UnicodeEncoding && (position & 1) != 0)
{
throw new InvalidDataException("UTF-16 encoding provided, but stream has odd length.");
}
// Allow up to two bytes for data from the start of the previous
// read which didn't quite make it as full characters
byte[] buffer = new byte[bufferSize + 2];
char[] charBuffer = new char[encoding.GetMaxCharCount(buffer.Length)];
int leftOverData = 0;
String? previousEnd = null;
// TextReader doesn't return an empty string if there's line break at the end
// of the data. Therefore we don't return an empty string if it's our *first*
// return.
bool firstYield = true;
// A line-feed at the start of the previous buffer means we need to swallow
// the carriage-return at the end of this buffer - hence this needs declaring
// way up here!
bool swallowCarriageReturn = false;
while (position > 0)
{
int bytesToRead = Math.Min(position > int.MaxValue ? bufferSize : (int)position, bufferSize);
position -= bytesToRead;
stream.Position = position;
StreamUtil.ReadExactly(stream, buffer, bytesToRead);
// If we haven't read a full buffer, but we had bytes left
// over from before, copy them to the end of the buffer
if (leftOverData > 0 && bytesToRead != bufferSize)
{
// Buffer.BlockCopy doesn't document its behaviour with respect
// to overlapping data: we *might* just have read 7 bytes instead of
// 8, and have two bytes to copy...
Array.Copy(buffer, bufferSize, buffer, bytesToRead, leftOverData);
}
// We've now *effectively* read this much data.
bytesToRead += leftOverData;
int firstCharPosition = 0;
while (!characterStartDetector(position + firstCharPosition, buffer[firstCharPosition]))
{
firstCharPosition++;
// Bad UTF-8 sequences could trigger this. For UTF-8 we should always
// see a valid character start in every 3 bytes, and if this is the start of the file
// so we've done a short read, we should have the character start
// somewhere in the usable buffer.
if (firstCharPosition == 3 || firstCharPosition == bytesToRead)
{
throw new InvalidDataException("Invalid UTF-8 data");
}
}
leftOverData = firstCharPosition;
int charsRead = encoding.GetChars(buffer, firstCharPosition, bytesToRead - firstCharPosition, charBuffer, 0);
int endExclusive = charsRead;
for (int i = charsRead - 1; i >= 0; i--)
{
char lookingAt = charBuffer[i];
if (swallowCarriageReturn)
{
swallowCarriageReturn = false;
if (lookingAt == '\r')
{
endExclusive--;
continue;
}
}
// Anything non-line-breaking, just keep looking backwards
if (lookingAt != '\n' && lookingAt != '\r')
{
continue;
}
// End of CRLF? Swallow the preceding CR
if (lookingAt == '\n')
{
swallowCarriageReturn = true;
}
int start = i + 1;
string bufferContents = new string(charBuffer, start, endExclusive - start);
endExclusive = i;
string stringToYield = previousEnd == null ? bufferContents : bufferContents + previousEnd;
if (!firstYield || stringToYield.Length != 0)
{
yield return stringToYield;
}
firstYield = false;
previousEnd = null;
}
previousEnd = endExclusive == 0 ? null : (new string(charBuffer, 0, endExclusive) + previousEnd);
// If we didn't decode the start of the array, put it at the end for next time
if (leftOverData != 0)
{
Buffer.BlockCopy(buffer, 0, buffer, bufferSize, leftOverData);
}
}
if (leftOverData != 0)
{
// At the start of the final buffer, we had the end of another character.
throw new InvalidDataException("Invalid UTF-8 data at start of stream");
}
if (firstYield && string.IsNullOrEmpty(previousEnd))
{
yield break;
}
yield return previousEnd ?? "";
}
finally
{
stream.Dispose();
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static class StreamUtil
{
public static void ReadExactly(Stream input, byte[] buffer, int bytesToRead)
{
int index = 0;
while (index < bytesToRead)
{
int read = input.Read(buffer, index, bytesToRead - index);
if (read == 0)
{
throw new EndOfStreamException
(String.Format("End of stream reached with {0} byte{1} left to read.",
bytesToRead - index,
bytesToRead - index == 1 ? "s" : ""));
}
index += read;
}
}
}
}

View File

@ -34,6 +34,8 @@ namespace Bloxstrap.Helpers
File.Delete(Directories.App); File.Delete(Directories.App);
File.Copy(Environment.ProcessPath, Directories.App); File.Copy(Environment.ProcessPath, Directories.App);
Bootstrapper.Register();
Program.ShowMessageBox( Program.ShowMessageBox(
$"{Program.ProjectName} has been updated to v{currentVersionInfo.ProductVersion}", $"{Program.ProjectName} has been updated to v{currentVersionInfo.ProductVersion}",
MessageBoxIcon.Information, MessageBoxIcon.Information,
@ -42,7 +44,7 @@ namespace Bloxstrap.Helpers
new Preferences().ShowDialog(); new Preferences().ShowDialog();
Environment.Exit(0); Program.Exit();
} }
} }
@ -51,7 +53,7 @@ namespace Bloxstrap.Helpers
public static async Task Check() public static async Task Check()
{ {
if (Environment.ProcessPath is null) if (Environment.ProcessPath is null || Program.IsUninstall || Program.IsQuiet && Program.IsFirstRun)
return; return;
if (!Program.IsFirstRun) if (!Program.IsFirstRun)
@ -84,7 +86,7 @@ namespace Bloxstrap.Helpers
if (result == DialogResult.Yes) if (result == DialogResult.Yes)
{ {
Utilities.OpenWebsite($"https://github.com/{Program.ProjectRepository}/releases/latest"); Utilities.OpenWebsite($"https://github.com/{Program.ProjectRepository}/releases/latest");
Program.Exit(); Program.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
} }
} }
} }

View File

@ -15,14 +15,9 @@ namespace Bloxstrap.Helpers
public static async Task<T?> GetJson<T>(string url) public static async Task<T?> GetJson<T>(string url)
{ {
using (HttpClient client = new()) string json = await Program.HttpClient.GetStringAsync(url);
{
client.DefaultRequestHeaders.Add("User-Agent", Program.ProjectRepository);
string json = await client.GetStringAsync(url);
return JsonSerializer.Deserialize<T>(json); return JsonSerializer.Deserialize<T>(json);
} }
}
public static string MD5File(string filename) public static string MD5File(string filename)
{ {

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Bloxstrap.Models
{
public class ClientVersion
{
[JsonPropertyName("version")]
public string Version { get; set; } = null!;
[JsonPropertyName("clientVersionUpload")]
public string VersionGuid { get; set; } = null!;
[JsonPropertyName("bootstrapperVersion")]
public string BootstrapperVersion { get; set; } = null!;
public DateTime? Timestamp { get; set; }
}
}

View File

@ -4,6 +4,9 @@ namespace Bloxstrap.Models
{ {
public class GithubRelease public class GithubRelease
{ {
[JsonPropertyName("tag_name")]
public string TagName { get; set; } = null!;
[JsonPropertyName("name")] [JsonPropertyName("name")]
public string Name { get; set; } = null!; public string Name { get; set; } = null!;

View File

@ -19,6 +19,8 @@ namespace Bloxstrap.Models
public bool RFUEnabled { get; set; } = false; public bool RFUEnabled { get; set; } = false;
public bool RFUAutoclose { get; set; } = false; public bool RFUAutoclose { get; set; } = false;
public string RFUVersion { get; set; } = "";
public bool UseOldDeathSound { get; set; } = true; public bool UseOldDeathSound { get; set; } = true;
public bool UseOldMouseCursor { get; set; } = false; public bool UseOldMouseCursor { get; set; } = false;
public bool UseDisableAppPatch { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false;

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models
{
public class VersionDeploy
{
public string VersionGuid { get; set; } = null!;
public string FileVersion { get; set; } = null!;
public DateTime Timestamp { get; set; }
}
}

View File

@ -8,6 +8,11 @@ using Bloxstrap.Enums;
using Bloxstrap.Helpers; using Bloxstrap.Helpers;
using Bloxstrap.Models; using Bloxstrap.Models;
using Bloxstrap.Dialogs; using Bloxstrap.Dialogs;
using System.Net.Http;
using System.Net;
using System.Reflection;
using Newtonsoft.Json.Linq;
using System;
namespace Bloxstrap namespace Bloxstrap
{ {
@ -28,23 +33,32 @@ namespace Bloxstrap
public static string BaseDirectory = null!; public static string BaseDirectory = null!;
public static bool IsFirstRun { get; private set; } = false; public static bool IsFirstRun { get; private set; } = false;
public static bool IsQuiet { get; private set; } = false;
public static bool IsUninstall { get; private set; } = false;
public static bool IsNoLaunch { get; private set; } = false;
public static string LocalAppData { get; private set; } = null!; public static string LocalAppData { get; private set; } = null!;
public static string StartMenu { get; private set; } = null!; public static string StartMenu { get; private set; } = null!;
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static SettingsManager SettingsManager = new(); public static SettingsManager SettingsManager = new();
public static SettingsFormat Settings = SettingsManager.Settings; public static SettingsFormat Settings = SettingsManager.Settings;
public static readonly HttpClient HttpClient = new(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All });
// shorthand // shorthand
public static DialogResult ShowMessageBox(string message, MessageBoxIcon icon = MessageBoxIcon.None, MessageBoxButtons buttons = MessageBoxButtons.OK) public static DialogResult ShowMessageBox(string message, MessageBoxIcon icon = MessageBoxIcon.None, MessageBoxButtons buttons = MessageBoxButtons.OK)
{ {
if (IsQuiet)
return DialogResult.None;
return MessageBox.Show(message, ProjectName, buttons, icon); return MessageBox.Show(message, ProjectName, buttons, icon);
} }
public static void Exit() public static void Exit(int code = Bootstrapper.ERROR_SUCCESS)
{ {
SettingsManager.Save(); SettingsManager.Save();
Environment.Exit(0); Environment.Exit(code);
} }
/// <summary> /// <summary>
@ -57,9 +71,24 @@ namespace Bloxstrap
// see https://aka.ms/applicationconfiguration. // see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
HttpClient.Timeout = TimeSpan.FromMinutes(5);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
LocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); LocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
StartMenu = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProjectName); StartMenu = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProjectName);
if (args.Length > 0)
{
if (Array.IndexOf(args, "-quiet") != -1)
IsQuiet = true;
if (Array.IndexOf(args, "-uninstall") != -1)
IsUninstall = true;
if (Array.IndexOf(args, "-nolaunch") != -1)
IsNoLaunch = true;
}
// check if installed // check if installed
RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"); RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}");
@ -67,6 +96,10 @@ namespace Bloxstrap
{ {
IsFirstRun = true; IsFirstRun = true;
Settings = SettingsManager.Settings; Settings = SettingsManager.Settings;
if (IsQuiet)
BaseDirectory = Path.Combine(LocalAppData, ProjectName);
else
new Preferences().ShowDialog(); new Preferences().ShowDialog();
} }
else else
@ -135,7 +168,6 @@ namespace Bloxstrap
} }
#endif #endif
if (!String.IsNullOrEmpty(commandLine)) if (!String.IsNullOrEmpty(commandLine))
{ {
DeployManager.Channel = Settings.Channel; DeployManager.Channel = Settings.Channel;

@ -1 +1 @@
Subproject commit a9fcc8d1e85738bc6493474a62a961842fa8dbc3 Subproject commit db3667e9749a82a3690539e622ff921771164af0

View File

@ -1,9 +1,12 @@
# <img src="https://github.com/pizzaboxer/bloxstrap/raw/main/Bloxstrap/Resources/IconBloxstrap-png.png" width="48"/> Bloxstrap # <img src="https://github.com/pizzaboxer/bloxstrap/raw/main/Bloxstrap/Resources/IconBloxstrap-png.png" width="48"/> Bloxstrap
![License](https://img.shields.io/github/license/pizzaboxer/bloxstrap) ![Downloads](https://img.shields.io/github/downloads/pizzaboxer/bloxstrap/total) ![Star](https://img.shields.io/github/stars/pizzaboxer/bloxstrap?style=social) ![License](https://img.shields.io/github/license/pizzaboxer/bloxstrap)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/pizzaboxer/bloxstrap/ci.yml?branch=main)
![Downloads](https://img.shields.io/github/downloads/pizzaboxer/bloxstrap/total)
![Star](https://img.shields.io/github/stars/pizzaboxer/bloxstrap?style=social)
An open, customizable, feature-packed alternative bootstrapper for Roblox. An open-source, feature-packed alternative bootstrapper for Roblox.
This a drop-in replacement for the stock Roblox bootstrapper, working more or less how you'd expect it to, while providing additional useful features. This does not touch or modify the game client itself. It merely just serves as a launcher, so there's no risk of being banned for using this. This a drop-in replacement for the stock Roblox bootstrapper, working more or less how you'd expect it to, while providing additional useful features. Keep in mind - this does not touch or modify the game client itself, it's just a launcher!
If you encounter a bug, or would like to suggest a feature, please submit an issue! If you encounter a bug, or would like to suggest a feature, please submit an issue!
@ -12,9 +15,11 @@ Bloxstrap is only supported for PCs running Windows.
## Features ## Features
Here's some of the features that Bloxstrap provides over the stock Roblox bootstrapper: Here's some of the features that Bloxstrap provides over the stock Roblox bootstrapper:
* Support for persistent file modifications (e.g. re-adding the old death sound) * A customizable launcher (including dark theme!)
* Gives you the ability to opt-in to pre-release testing channels * Persistent file modifications (re-adds the old death sound!)
* Integration with Discord Rich Presence and [rbxfpsunlocker](https://github.com/axstin/rbxfpsunlocker) * Support for opting in to pre-release testing channels
* Painless Discord Rich Presence support
* Support for silent automatic FPS unlocking with [rbxfpsunlocker](https://github.com/axstin/rbxfpsunlocker)
## Installing ## Installing
Download the [latest release of Bloxstrap](https://github.com/pizzaboxer/bloxstrap/releases/latest), and run it. Configure your preferences if needed, and install. That's about it! Download the [latest release of Bloxstrap](https://github.com/pizzaboxer/bloxstrap/releases/latest), and run it. Configure your preferences if needed, and install. That's about it!
@ -26,7 +31,7 @@ It's not unlikely that Windows Smartscreen will show a popup when you run Bloxst
Once installed, Bloxstrap is added to your Start Menu, where you can change your preferences if needed. Once installed, Bloxstrap is added to your Start Menu, where you can change your preferences if needed.
## Contributions ## Contributions
* [Roblox Studio Mod Manager](https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager) - some slightly modified utility code was borrowed to help with Bloxstrap's bootstrapper functionality (Bloxstrap.Helpers.RSMM). Besides, it's a great project. * [Roblox Studio Mod Manager](https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager) - some utilities was borrowed to help with Bloxstrap's bootstrapper functionality. Besides, it's a great project.
* [skulyire](https://www.roblox.com/users/2485612194/profile) - Making the Bloxstrap logo. * [skulyire](https://www.roblox.com/users/2485612194/profile) - Making the Bloxstrap logo.
* [rbxfpsunlocker](https://github.com/axstin/rbxfpsunlocker) - Added as a Bloxstrap integration. * [rbxfpsunlocker](https://github.com/axstin/rbxfpsunlocker) - Used for Bloxstrap's FPS unlocking.
* [WPFDarkTheme](https://github.com/AngryCarrot789/WPFDarkTheme) - Used for making the Preferences menu. * [WPFDarkTheme](https://github.com/AngryCarrot789/WPFDarkTheme) - Used for making the Preferences menu.