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>
<Platforms>AnyCPU;x86</Platforms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>1.5.3</Version>
<FileVersion>1.5.3.0</FileVersion>
<Version>1.5.4</Version>
<FileVersion>1.5.4.0</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -10,12 +10,21 @@ using Bloxstrap.Dialogs.BootstrapperDialogs;
using Bloxstrap.Helpers;
using Bloxstrap.Helpers.Integrations;
using Bloxstrap.Helpers.RSMM;
using Bloxstrap.Models;
using System.Net;
namespace Bloxstrap
{
public partial class Bootstrapper
{
#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
// 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?
@ -64,8 +73,6 @@ namespace Bloxstrap
"Any files added here to the root modification directory are ignored, so be sure\n" +
"that they're inside a folder.";
private static readonly HttpClient Client = new();
private string? LaunchCommandLine;
private string VersionGuid = null!;
@ -85,13 +92,15 @@ namespace Bloxstrap
{
LaunchCommandLine = launchCommandLine;
FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid);
Client.Timeout = TimeSpan.FromMinutes(10);
}
// this is called from BootstrapperStyleForm.SetupDialog()
public async Task Run()
{
if (LaunchCommandLine == "-uninstall")
if (Program.IsQuiet)
Dialog.CloseDialog();
if (Program.IsUninstall)
{
Uninstall();
return;
@ -116,9 +125,12 @@ namespace Bloxstrap
await RbxFpsUnlocker.CheckInstall();
//if (Program.IsFirstRun)
// Dialog.ShowSuccess($"{Program.ProjectName} has been installed");
//else
Program.SettingsManager.Save();
if (Program.IsFirstRun && Program.IsNoLaunch)
Dialog.ShowSuccess($"{Program.ProjectName} has successfully installed");
else if (!Program.IsNoLaunch)
await StartRoblox();
Program.Exit();
@ -128,7 +140,8 @@ namespace Bloxstrap
{
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);
VersionPackageManifest = await PackageManifest.Get(VersionGuid);
}
@ -236,7 +249,7 @@ namespace Bloxstrap
{
if (!Dialog.CancelEnabled)
{
Program.Exit();
Program.Exit(ERROR_INSTALL_USEREXIT);
return;
}
@ -251,7 +264,7 @@ namespace Bloxstrap
}
catch (Exception) { }
Program.Exit();
Program.Exit(ERROR_INSTALL_USEREXIT);
}
#endregion
@ -281,11 +294,16 @@ namespace Bloxstrap
RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}");
uninstallKey.SetValue("DisplayIcon", $"{Directories.App},0");
uninstallKey.SetValue("DisplayName", Program.ProjectName);
uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
uninstallKey.SetValue("DisplayVersion", Program.Version);
if (uninstallKey.GetValue("InstallDate") is null)
uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
uninstallKey.SetValue("InstallLocation", Directories.Base);
uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", Program.ProjectName);
uninstallKey.SetValue("Publisher", "pizzaboxer");
uninstallKey.SetValue("ModifyPath", $"\"{Directories.App}\" -preferences");
uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.App}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Directories.App}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{Program.ProjectRepository}");
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Program.ProjectRepository}/releases/latest");
@ -367,6 +385,8 @@ namespace Bloxstrap
catch (Exception) { }
Dialog.ShowSuccess($"{Program.ProjectName} has been uninstalled");
Environment.Exit(ERROR_PRODUCT_UNINSTALLED);
}
#endregion
@ -607,7 +627,7 @@ namespace Bloxstrap
{
Debug.WriteLine($"Downloading {package.Name}...");
var response = await Client.GetAsync(packageUrl);
var response = await Program.HttpClient.GetAsync(packageUrl);
if (CancelFired)
return;

View File

@ -61,6 +61,9 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
public void SetupDialog()
{
if (Program.IsQuiet)
this.Hide();
this.Text = Program.ProjectName;
this.Icon = Program.Settings.BootstrapperIcon.GetIcon();
@ -107,7 +110,7 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
public virtual void ShowError(string message)
{
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()
@ -127,7 +130,7 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
);
if (result != DialogResult.OK)
Environment.Exit(0);
Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
}
public void ButtonCancel_Click(object? sender, EventArgs e)

View File

@ -41,7 +41,7 @@
this.ShowInTaskbar = false;
this.Text = "VistaDialog";
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);
}

View File

@ -101,7 +101,9 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
successDialog.Buttons[0].Click += (sender, e) => Program.Exit();
Dialog.Navigate(successDialog);
if (!Program.IsQuiet)
Dialog.Navigate(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;
}
}
@ -152,9 +156,10 @@ namespace Bloxstrap.Dialogs.BootstrapperDialogs
}
private void TestDialog_Load(object sender, EventArgs e)
private void VistaDialog_Load(object sender, EventArgs e)
{
TaskDialog.ShowDialog(Dialog);
if (!Program.IsQuiet)
TaskDialog.ShowDialog(Dialog);
}
}
}

View File

@ -34,7 +34,7 @@
<GroupBox Grid.Column="0" Header="Discord Rich Presence" Margin="10,10,5,5">
<StackPanel VerticalAlignment="Center">
<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>
</GroupBox>
<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";
VersionDeploy info = await DeployManager.GetLastDeploy(channel);
string strTimestamp = info.Timestamp.ToString("MM/dd/yyyy h:mm:ss tt", Program.CultureFormat);
ClientVersion info = await DeployManager.GetLastDeploy(channel, true);
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;
namespace Bloxstrap.Helpers
@ -18,76 +17,30 @@ namespace Bloxstrap.Helpers
public static readonly List<string> ChannelsAbstracted = new List<string>()
{
"LIVE",
"ZAvatarTeam",
"ZCanary",
//"ZFeatureHarmony", last updated 9/20, shouldn't be here anymore
"ZFlag",
"ZIntegration",
"ZLive",
"ZNext",
//"ZPublic",
"ZSocialTeam"
"ZCanary",
"ZIntegration"
};
// why not?
public static readonly List<string> ChannelsAll = new List<string>()
{
"LIVE",
"Ganesh",
"ZAvatarTeam",
"ZBugFixBoost-Mutex-Revert",
"ZBugFixCLI-54676-Test",
"ZBugFixCLI-55214-Master",
"ZAvatarRelease",
"ZCanary",
"ZCanary1",
"ZCanary2",
"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",
"ZIntegration",
"ZIntegration1",
"ZLang",
"ZLive",
"ZLive1",
"ZLoom",
"ZNext",
"ZProject512-Boost-Remove-Mutex-1",
"ZProject516-Boost-Remove-Mutex-Network",
"ZPublic",
"ZQtitanStudio",
"ZQTitanStudioRelease",
"ZReleaseVS2019",
"ZSocialTeam",
"ZStIntegration",
"ZStudioInt1",
"ZStudioInt2",
"ZStudioInt3",
"ZStudioInt4",
"ZStudioInt5",
"ZStudioInt6",
"ZStudioInt7",
"ZStudioInt8",
"ZTesting",
"ZVS2019"
"ZStudioInt2"
};
#endregion
@ -99,70 +52,37 @@ namespace Bloxstrap.Helpers
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);
string lastDeploy = "";
HttpResponseMessage deployInfoResponse = await Program.HttpClient.GetAsync($"https://clientsettings.roblox.com/v2/client-version/WindowsPlayer/channel/{channel}");
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.
// 500 = Error while fetching version information.
// either way, we throw
throw new Exception($"Could not get latest deploy for channel {channel}");
}
using (StringReader reader = new(deployHistory))
string rawJson = await deployInfoResponse.Content.ReadAsStringAsync();
ClientVersion clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawJson)!;
// for preferences
if (timestamp)
{
string channelUrl = BuildBaseUrl(channel);
// get an approximate deploy time from rbxpkgmanifest's last modified date
HttpResponseMessage pkgResponse = await Program.HttpClient.GetAsync($"{channelUrl}/{clientVersion.VersionGuid}-rbxPkgManifest.txt");
if (pkgResponse.Content.Headers.TryGetValues("last-modified", out var values))
{
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
if (line.Contains("WindowsPlayer"))
lastDeploy = line;
}
string lastModified = values.First();
clientVersion.Timestamp = DateTime.Parse(lastModified);
}
}
if (String.IsNullOrEmpty(lastDeploy))
throw new Exception($"Could not get latest deploy for channel {channel}");
// here's to hoping roblox doesn't change their deployment entry format
// (last time they did so was may 2021 so we should be fine?)
// 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
// this here is the lazy way but it should just work™
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 ...'
fileVersion = lastDeploy[..lastDeploy.IndexOf(", git")]; // '0, 542, 100, 5420251'
}
else
{
// pre-may 2021 entry: ends like 'file version: 0, 448, 0, 411122...'
fileVersion = lastDeploy[..lastDeploy.IndexOf("...")]; // '0, 448, 0, 411122'
}
// deployment timestamps are UTC-5
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
};
return clientVersion;
}
}
}

View File

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

View File

@ -70,25 +70,19 @@ namespace Bloxstrap.Helpers.Integrations
return;
}
DateTime lastReleasePublish;
string downloadUrl;
var releaseInfo = await Utilities.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest");
if (releaseInfo is null || releaseInfo.Assets is null)
return;
lastReleasePublish = DateTime.Parse(releaseInfo.CreatedAt);
downloadUrl = releaseInfo.Assets[0].BrowserDownloadUrl;
string downloadUrl = releaseInfo.Assets[0].BrowserDownloadUrl;
Directory.CreateDirectory(folderLocation);
if (File.Exists(fileLocation))
{
DateTime lastDownload = File.GetCreationTimeUtc(fileLocation);
// no new release published, return
if (lastDownload > lastReleasePublish)
if (Program.Settings.RFUVersion == releaseInfo.TagName)
return;
CheckIfRunning();
@ -97,21 +91,20 @@ namespace Bloxstrap.Helpers.Integrations
Debug.WriteLine("Installing/Updating rbxfpsunlocker...");
using (HttpClient client = new())
{
byte[] bytes = await client.GetByteArrayAsync(downloadUrl);
byte[] bytes = await Program.HttpClient.GetByteArrayAsync(downloadUrl);
using (MemoryStream zipStream = new(bytes))
{
ZipArchive zip = new(zipStream);
zip.ExtractToDirectory(folderLocation, true);
}
using (MemoryStream zipStream = new(bytes))
{
ZipArchive zip = new(zipStream);
zip.ExtractToDirectory(folderLocation, true);
}
if (!File.Exists(settingsLocation))
{
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 pkgManifestData;
using (HttpClient http = new())
{
var getData = http.GetStringAsync(pkgManifestUrl);
pkgManifestData = await getData.ConfigureAwait(false);
}
var getData = Program.HttpClient.GetStringAsync(pkgManifestUrl);
pkgManifestData = await getData.ConfigureAwait(false);
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.Copy(Environment.ProcessPath, Directories.App);
Bootstrapper.Register();
Program.ShowMessageBox(
$"{Program.ProjectName} has been updated to v{currentVersionInfo.ProductVersion}",
MessageBoxIcon.Information,
@ -42,7 +44,7 @@ namespace Bloxstrap.Helpers
new Preferences().ShowDialog();
Environment.Exit(0);
Program.Exit();
}
}
@ -51,7 +53,7 @@ namespace Bloxstrap.Helpers
public static async Task Check()
{
if (Environment.ProcessPath is null)
if (Environment.ProcessPath is null || Program.IsUninstall || Program.IsQuiet && Program.IsFirstRun)
return;
if (!Program.IsFirstRun)
@ -84,7 +86,7 @@ namespace Bloxstrap.Helpers
if (result == DialogResult.Yes)
{
Utilities.OpenWebsite($"https://github.com/{Program.ProjectRepository}/releases/latest");
Program.Exit();
Program.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
}
}
}

View File

@ -15,13 +15,8 @@ namespace Bloxstrap.Helpers
public static async Task<T?> GetJson<T>(string url)
{
using (HttpClient client = new())
{
client.DefaultRequestHeaders.Add("User-Agent", Program.ProjectRepository);
string json = await client.GetStringAsync(url);
return JsonSerializer.Deserialize<T>(json);
}
string json = await Program.HttpClient.GetStringAsync(url);
return JsonSerializer.Deserialize<T>(json);
}
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
{
[JsonPropertyName("tag_name")]
public string TagName { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;

View File

@ -19,6 +19,8 @@ namespace Bloxstrap.Models
public bool RFUEnabled { get; set; } = false;
public bool RFUAutoclose { get; set; } = false;
public string RFUVersion { get; set; } = "";
public bool UseOldDeathSound { get; set; } = true;
public bool UseOldMouseCursor { 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.Models;
using Bloxstrap.Dialogs;
using System.Net.Http;
using System.Net;
using System.Reflection;
using Newtonsoft.Json.Linq;
using System;
namespace Bloxstrap
{
@ -28,23 +33,32 @@ namespace Bloxstrap
public static string BaseDirectory = null!;
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 StartMenu { get; private set; } = null!;
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static SettingsManager SettingsManager = new();
public static SettingsFormat Settings = SettingsManager.Settings;
public static readonly HttpClient HttpClient = new(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All });
// shorthand
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);
}
public static void Exit()
public static void Exit(int code = Bootstrapper.ERROR_SUCCESS)
{
SettingsManager.Save();
Environment.Exit(0);
Environment.Exit(code);
}
/// <summary>
@ -57,17 +71,36 @@ namespace Bloxstrap
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
HttpClient.Timeout = TimeSpan.FromMinutes(5);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
LocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
StartMenu = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProjectName);
// check if installed
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
RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}");
if (registryKey is null)
{
IsFirstRun = true;
Settings = SettingsManager.Settings;
new Preferences().ShowDialog();
if (IsQuiet)
BaseDirectory = Path.Combine(LocalAppData, ProjectName);
else
new Preferences().ShowDialog();
}
else
{
@ -135,7 +168,6 @@ namespace Bloxstrap
}
#endif
if (!String.IsNullOrEmpty(commandLine))
{
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
![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!
@ -12,9 +15,11 @@ Bloxstrap is only supported for PCs running Windows.
## Features
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)
* Gives you the ability to opt-in to pre-release testing channels
* Integration with Discord Rich Presence and [rbxfpsunlocker](https://github.com/axstin/rbxfpsunlocker)
* A customizable launcher (including dark theme!)
* Persistent file modifications (re-adds the old death sound!)
* 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
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.
## 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.
* [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.