Merge pull request #422 from pizzaboxer/version-2.4.0

Version 2.4.0
This commit is contained in:
pizzaboxer 2023-07-24 21:19:23 +01:00 committed by GitHub
commit 34bba491fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
165 changed files with 6375 additions and 3925 deletions

View File

@ -6,28 +6,33 @@ jobs:
strategy: strategy:
matrix: matrix:
configuration: [Debug, Release] configuration: [Debug, Release]
platform: [x64]
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.x' dotnet-version: '6.0.x'
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
run: dotnet build --no-restore run: dotnet build --no-restore
- name: Publish - name: Publish
run: dotnet publish -p:PublishSingleFile=true -r win-${{ matrix.platform }} -c ${{ matrix.configuration }} --self-contained false .\Bloxstrap\Bloxstrap.csproj run: dotnet publish -p:PublishSingleFile=true -p:CommitHash=${{ github.sha }} -p:CommitRef=${{ github.ref_type }}/${{ github.ref_name }} -r win-x64 -c ${{ matrix.configuration }} --self-contained false .\Bloxstrap\Bloxstrap.csproj
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: Bloxstrap (${{ matrix.configuration }}, ${{ matrix.platform }}) name: Bloxstrap (${{ matrix.configuration }})
path: | path: |
.\Bloxstrap\bin\${{ matrix.configuration }}\net6.0-windows\win-${{ matrix.platform }}\publish\* .\Bloxstrap\bin\${{ matrix.configuration }}\net6.0-windows\win-x64\publish\*
release: release:
needs: build needs: build
@ -38,15 +43,17 @@ jobs:
- name: Download x64 release artifact - name: Download x64 release artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: Bloxstrap (Release, x64) name: Bloxstrap (Release)
path: x64 path: x64
- name: Rename binaries - name: Rename binaries
run: | run: |
mv x64/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}-x64.exe mv x64/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}.exe
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
draft: true draft: true
files: | files: |
Bloxstrap-${{ github.ref_name }}-x64.exe Bloxstrap-${{ github.ref_name }}.exe
name: Bloxstrap ${{ github.ref_name }} name: Bloxstrap ${{ github.ref_name }}

View File

@ -11,6 +11,8 @@
<ui:ThemesDictionary Theme="Dark" /> <ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary /> <ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<FontFamily x:Key="Rubik">pack://application:,,,/Resources/Fonts/#Rubik Light</FontFamily>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@ -1,349 +1,343 @@
using System; using System.Reflection;
using System.Diagnostics; using System.Windows;
using System.Globalization; using System.Windows.Threading;
using System.IO;
using System.Linq; using Microsoft.Win32;
using System.Net.Http;
using System.Net; namespace Bloxstrap
using System.Reflection; {
using System.Threading; /// <summary>
using System.Threading.Tasks; /// Interaction logic for App.xaml
using System.Windows; /// </summary>
using System.Windows.Threading; public partial class App : Application
{
using Microsoft.Win32; public const string ProjectName = "Bloxstrap";
public const string ProjectRepository = "pizzaboxer/bloxstrap";
using Bloxstrap.Dialogs; public const string RobloxAppName = "RobloxPlayerBeta";
using Bloxstrap.Extensions;
using Bloxstrap.Models; // used only for communicating between app and menu - use Directories.Base for anything else
using Bloxstrap.Singletons; public static string BaseDirectory = null!;
using Bloxstrap.Views;
public static bool ShouldSaveConfigs { get; set; } = false;
namespace Bloxstrap public static bool IsSetupComplete { get; set; } = true;
{ public static bool IsFirstRun { get; private set; } = true;
/// <summary> public static bool IsQuiet { get; private set; } = false;
/// Interaction logic for App.xaml public static bool IsUninstall { get; private set; } = false;
/// </summary> public static bool IsNoLaunch { get; private set; } = false;
public partial class App : Application public static bool IsUpgrade { get; private set; } = false;
{ public static bool IsMenuLaunch { get; private set; } = false;
public static readonly CultureInfo CultureFormat = CultureInfo.InvariantCulture; public static string[] LaunchArgs { get; private set; } = null!;
public const string ProjectName = "Bloxstrap"; public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute<BuildMetadataAttribute>()!;
public const string ProjectRepository = "pizzaboxer/bloxstrap"; public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
// used only for communicating between app and menu - use Directories.Base for anything else public static NotifyIconWrapper? NotifyIcon { get; private set; }
public static string BaseDirectory = null!;
public static bool ShouldSaveConfigs { get; set; } = false; public static readonly Logger Logger = new();
public static bool IsSetupComplete { get; set; } = true;
public static bool IsFirstRun { get; private set; } = true; public static readonly JsonManager<Settings> Settings = new();
public static bool IsQuiet { get; private set; } = false; public static readonly JsonManager<State> State = new();
public static bool IsUninstall { get; private set; } = false; public static readonly FastFlagManager FastFlags = new();
public static bool IsNoLaunch { get; private set; } = false;
public static bool IsUpgrade { get; private set; } = false; public static readonly HttpClient HttpClient = new(new HttpClientLoggingHandler(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }));
public static bool IsMenuLaunch { get; private set; } = false;
public static string[] LaunchArgs { get; private set; } = null!; public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; if (IsFirstRun)
{
// singletons if (exitCode == ErrorCode.ERROR_CANCELLED)
public static readonly Logger Logger = new(); exitCode = ErrorCode.ERROR_INSTALL_USEREXIT;
public static readonly JsonManager<Settings> Settings = new(); }
public static readonly JsonManager<State> State = new();
public static readonly FastFlagManager FastFlags = new(); int exitCodeNum = (int)exitCode;
public static readonly HttpClient HttpClient = new(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All });
Logger.WriteLine($"[App::Terminate] Terminating with exit code {exitCodeNum} ({exitCode})");
public static System.Windows.Forms.NotifyIcon Notification { get; private set; } = null!;
Settings.Save();
// shorthand State.Save();
public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK) NotifyIcon?.Dispose();
{
if (IsQuiet) Environment.Exit(exitCodeNum);
return MessageBoxResult.None; }
return MessageBox.Show(message, ProjectName, buttons, icon); void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e)
} {
e.Handled = true;
public static void Terminate(int code = Bootstrapper.ERROR_SUCCESS)
{ Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread");
Logger.WriteLine($"[App::Terminate] Terminating with exit code {code}"); Logger.WriteLine($"[App::OnStartup] {e.Exception}");
Settings.Save();
State.Save(); FinalizeExceptionHandling(e.Exception);
Environment.Exit(code); }
}
void FinalizeExceptionHandling(Exception exception)
private void InitLog() {
{ #if DEBUG
// if we're running for the first time or uninstalling, log to temp folder throw exception;
// else, log to bloxstrap folder #else
if (!IsQuiet)
bool isUsingTempDir = IsFirstRun || IsUninstall; Controls.ShowExceptionDialog(exception);
string logdir = isUsingTempDir ? Path.Combine(Directories.LocalAppData, "Temp") : Path.Combine(Directories.Base, "Logs");
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
#endif
Logger.Initialize(Path.Combine(logdir, $"{ProjectName}_{timestamp}.log")); }
// clean up any logs older than a week protected override void OnStartup(StartupEventArgs e)
if (!isUsingTempDir) {
{ base.OnStartup(e);
foreach (FileInfo log in new DirectoryInfo(logdir).GetFiles())
{ Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}");
if (log.LastWriteTimeUtc.AddDays(7) > DateTime.UtcNow)
continue; if (String.IsNullOrEmpty(BuildMetadata.CommitHash))
Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
Logger.WriteLine($"[App::InitLog] Cleaning up old log file '{log.Name}'"); else
log.Delete(); Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})");
}
} // To customize application configuration such as set high DPI settings or default font,
} // see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
void GlobalExceptionHandler(object sender, DispatcherUnhandledExceptionEventArgs e)
{ LaunchArgs = e.Args;
e.Handled = true;
HttpClient.Timeout = TimeSpan.FromMinutes(5);
Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread"); HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
Logger.WriteLine($"[App::OnStartup] {e.Exception}");
if (LaunchArgs.Length > 0)
if (!IsQuiet) {
Settings.Prop.BootstrapperStyle.GetNew().ShowError($"{e.Exception.GetType()}: {e.Exception.Message}"); if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1)
{
Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); Logger.WriteLine("[App::OnStartup] Started with IsMenuLaunch flag");
} IsMenuLaunch = true;
}
protected override void OnStartup(StartupEventArgs e)
{ if (Array.IndexOf(LaunchArgs, "-quiet") != -1)
base.OnStartup(e); {
Logger.WriteLine("[App::OnStartup] Started with IsQuiet flag");
Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}"); IsQuiet = true;
}
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration. if (Array.IndexOf(LaunchArgs, "-uninstall") != -1)
ApplicationConfiguration.Initialize(); {
Logger.WriteLine("[App::OnStartup] Started with IsUninstall flag");
LaunchArgs = e.Args; IsUninstall = true;
}
HttpClient.Timeout = TimeSpan.FromMinutes(5);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1)
{
if (LaunchArgs.Length > 0) Logger.WriteLine("[App::OnStartup] Started with IsNoLaunch flag");
{ IsNoLaunch = true;
if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1) }
{
Logger.WriteLine("[App::OnStartup] Started with IsMenuLaunch flag"); if (Array.IndexOf(LaunchArgs, "-upgrade") != -1)
IsMenuLaunch = true; {
} Logger.WriteLine("[App::OnStartup] Bloxstrap started with IsUpgrade flag");
IsUpgrade = true;
if (Array.IndexOf(LaunchArgs, "-quiet") != -1) }
{ }
Logger.WriteLine("[App::OnStartup] Started with IsQuiet flag");
IsQuiet = true; // check if installed
} using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"))
{
if (Array.IndexOf(LaunchArgs, "-uninstall") != -1) string? installLocation = null;
{
Logger.WriteLine("[App::OnStartup] Started with IsUninstall flag"); if (registryKey is not null)
IsUninstall = true; installLocation = (string?)registryKey.GetValue("InstallLocation");
}
if (registryKey is null || installLocation is null)
if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1) {
{ Logger.WriteLine("[App::OnStartup] Running first-time install");
Logger.WriteLine("[App::OnStartup] Started with IsNoLaunch flag");
IsNoLaunch = true; BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName);
} Logger.Initialize(true);
if (Array.IndexOf(LaunchArgs, "-upgrade") != -1) if (!IsQuiet)
{ {
Logger.WriteLine("[App::OnStartup] Bloxstrap started with IsUpgrade flag"); IsSetupComplete = false;
IsUpgrade = true; FastFlags.Load();
} Controls.ShowMenu();
} }
}
// so this needs to be here because winforms moment else
// onclick events will not fire unless this is defined here in the main thread so uhhhhh {
// we'll show the icon if we're launching roblox since we're likely gonna be showing a IsFirstRun = false;
// bunch of notifications, and always showing it just makes the most sense i guess since it BaseDirectory = installLocation;
// indicates that bloxstrap is running, even in the background }
Notification = new() }
{
Icon = Bloxstrap.Properties.Resources.IconBloxstrap, // exit if we don't click the install button on installation
Text = ProjectName, if (!IsSetupComplete)
Visible = !IsMenuLaunch {
}; Logger.WriteLine("[App::OnStartup] Installation cancelled!");
Terminate(ErrorCode.ERROR_CANCELLED);
// check if installed }
using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"))
{ Directories.Initialize(BaseDirectory);
if (registryKey is null)
{ // we shouldn't save settings on the first run until the first installation is finished,
Logger.WriteLine("[App::OnStartup] Running first-time install"); // just in case the user decides to cancel the install
if (!IsFirstRun)
BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName); {
InitLog(); Logger.Initialize(IsUninstall);
if (!IsQuiet) if (!Logger.Initialized)
{ {
IsSetupComplete = false; Logger.WriteLine("[App::OnStartup] Possible duplicate launch detected, terminating.");
FastFlags.Load(); Terminate();
new MainWindow().ShowDialog(); }
}
} Settings.Load();
else State.Load();
{ FastFlags.Load();
IsFirstRun = false; }
BaseDirectory = (string)registryKey.GetValue("InstallLocation")!;
} if (!IsUninstall && !IsMenuLaunch)
} NotifyIcon = new();
// exit if we don't click the install button on installation #if !DEBUG
if (!IsSetupComplete) if (!IsUninstall && !IsFirstRun)
{ Updater.CheckInstalledVersion();
Logger.WriteLine("[App::OnStartup] Installation cancelled!"); #endif
Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
} string commandLine = "";
Directories.Initialize(BaseDirectory); if (IsMenuLaunch)
{
// we shouldn't save settings on the first run until the first installation is finished, Process? menuProcess = Process.GetProcesses().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault();
// just in case the user decides to cancel the install
if (!IsFirstRun) if (menuProcess is not null)
{ {
InitLog(); IntPtr handle = menuProcess.MainWindowHandle;
Settings.Load(); Logger.WriteLine($"[App::OnStartup] Found an already existing menu window with handle {handle}");
State.Load(); NativeMethods.SetForegroundWindow(handle);
FastFlags.Load(); }
} else
{
#if !DEBUG if (Process.GetProcessesByName(ProjectName).Length > 1 && !IsQuiet)
if (!IsUninstall && !IsFirstRun) Controls.ShowMessageBox(
Updater.CheckInstalledVersion(); $"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.",
#endif MessageBoxImage.Information
);
string commandLine = "";
Controls.ShowMenu();
if (IsMenuLaunch) }
{ }
Mutex mutex; else if (LaunchArgs.Length > 0)
{
try if (LaunchArgs[0].StartsWith("roblox-player:"))
{ {
mutex = Mutex.OpenExisting("Bloxstrap_MenuMutex"); commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]);
Logger.WriteLine("[App::OnStartup] Bloxstrap_MenuMutex mutex exists, aborting menu launch..."); }
Terminate(); else if (LaunchArgs[0].StartsWith("roblox:"))
} {
catch if (Settings.Prop.UseDisableAppPatch)
{ Controls.ShowMessageBox(
// no mutex exists, continue to opening preferences menu "Roblox was launched via a deeplink, however the desktop app is required for deeplink launching to work. Because you've opted to disable the desktop app, it will temporarily be re-enabled for this launch only.",
mutex = new(true, "Bloxstrap_MenuMutex"); MessageBoxImage.Information
} );
if (Utilities.GetProcessCount(ProjectName) > 1) commandLine = $"--app --deeplink {LaunchArgs[0]}";
ShowMessageBox($"{ProjectName} is currently running, likely as a background Roblox process. Please note that not all your changes will immediately apply until you close all currently open Roblox instances.", MessageBoxImage.Information); }
else
new MainWindow().ShowDialog(); {
} commandLine = "--app";
else if (LaunchArgs.Length > 0) }
{ }
if (LaunchArgs[0].StartsWith("roblox-player:")) else
{ {
commandLine = ProtocolHandler.ParseUri(LaunchArgs[0]); commandLine = "--app";
} }
else if (LaunchArgs[0].StartsWith("roblox:"))
{ if (!String.IsNullOrEmpty(commandLine))
commandLine = $"--app --deeplink {LaunchArgs[0]}"; {
} if (!IsFirstRun)
else ShouldSaveConfigs = true;
{
commandLine = "--app"; // start bootstrapper and show the bootstrapper modal if we're not running silently
} Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper");
} Bootstrapper bootstrapper = new(commandLine);
else IBootstrapperDialog? dialog = null;
{
commandLine = "--app"; if (!IsQuiet)
} {
Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog");
if (!String.IsNullOrEmpty(commandLine)) dialog = Settings.Prop.BootstrapperStyle.GetNew();
{ bootstrapper.Dialog = dialog;
if (!IsFirstRun) dialog.Bootstrapper = bootstrapper;
ShouldSaveConfigs = true; }
// start bootstrapper and show the bootstrapper modal if we're not running silently // handle roblox singleton mutex for multi-instance launching
Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper"); // note we're handling it here in the main thread and NOT in the
Bootstrapper bootstrapper = new(commandLine); // bootstrapper as handling mutexes in async contexts suuuuuucks
IBootstrapperDialog? dialog = null;
Mutex? singletonMutex = null;
if (!IsQuiet)
{ if (Settings.Prop.MultiInstanceLaunching)
Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog"); {
dialog = Settings.Prop.BootstrapperStyle.GetNew(); Logger.WriteLine("[App::OnStartup] Creating singleton mutex");
bootstrapper.Dialog = dialog;
dialog.Bootstrapper = bootstrapper; try
} {
Mutex.OpenExisting("ROBLOX_singletonMutex");
// handle roblox singleton mutex for multi-instance launching Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!");
// note we're handling it here in the main thread and NOT in the }
// bootstrapper as handling mutexes in async contexts suuuuuucks catch
{
Mutex? singletonMutex = null; // create the singleton mutex before the game client does
singletonMutex = new Mutex(true, "ROBLOX_singletonMutex");
if (Settings.Prop.MultiInstanceLaunching) }
{ }
Logger.WriteLine("[App::OnStartup] Creating singleton mutex");
Task bootstrapperTask = Task.Run(() => bootstrapper.Run());
try
{ bootstrapperTask.ContinueWith(t =>
Mutex.OpenExisting("ROBLOX_singletonMutex"); {
Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!"); Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished");
}
catch // notifyicon is blocking main thread, must be disposed here
{ NotifyIcon?.Dispose();
// create the singleton mutex before the game client does
singletonMutex = new Mutex(true, "ROBLOX_singletonMutex"); if (t.IsFaulted)
} Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper");
}
if (t.Exception is null)
// there's a bug here that i have yet to fix! return;
// sometimes the task just terminates when the bootstrapper hasn't
// actually finished, causing the bootstrapper to hang indefinitely Logger.WriteLine($"[App::OnStartup] {t.Exception}");
// i have no idea how the fuck this happens, but it happens like VERY
// rarely so i'm not too concerned by it Exception exception = t.Exception;
// maybe one day ill find out why it happens
Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t => #if !DEBUG
{ if (t.Exception.GetType().ToString() == "System.AggregateException")
Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished"); exception = t.Exception.InnerException!;
#endif
if (t.IsFaulted)
Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper"); FinalizeExceptionHandling(exception);
});
if (t.Exception is null)
return; // this ordering is very important as all wpf windows are shown as modal dialogs, mess it up and you'll end up blocking input to one of them
dialog?.ShowBootstrapper();
Logger.WriteLine($"[App::OnStartup] {t.Exception}");
if (!IsNoLaunch && Settings.Prop.EnableActivityTracking)
#if DEBUG NotifyIcon?.InitializeContextMenu();
throw t.Exception;
#else Logger.WriteLine($"[App::OnStartup] Waiting for bootstrapper task to finish");
var exception = t.Exception.InnerExceptions.Count >= 1 ? t.Exception.InnerExceptions[0] : t.Exception;
dialog?.ShowError($"{exception.GetType()}: {exception.Message}"); bootstrapperTask.Wait();
Terminate(Bootstrapper.ERROR_INSTALL_FAILURE);
#endif if (singletonMutex is not null)
}); {
Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed");
dialog?.ShowBootstrapper();
bootstrapperTask.Wait(); // we've got ownership of the roblox singleton mutex!
// if we stop running, everything will screw up once any more roblox instances launched
if (singletonMutex is not null) while (Process.GetProcessesByName("RobloxPlayerBeta").Any())
{ Thread.Sleep(5000);
Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed"); }
}
// we've got ownership of the roblox singleton mutex!
// if we stop running, everything will screw up once any more roblox instances launched Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating...");
while (Process.GetProcessesByName("RobloxPlayerBeta").Any())
Thread.Sleep(5000); Terminate();
} }
} }
}
Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating...");
Terminate();
}
}
}

View File

@ -7,25 +7,39 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms> <UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon> <ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>2.3.0</Version> <Version>2.4.0</Version>
<FileVersion>2.3.0.0</FileVersion> <FileVersion>2.4.0.0</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Resource Include="Bloxstrap.ico" /> <Resource Include="Bloxstrap.ico" />
<Resource Include="Resources\Fonts\Rubik-VariableFont_wght.ttf" />
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoDark.jpg" />
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoLight.jpg" />
<Resource Include="Resources\Menu\StartMenu.png" />
<Resource Include="Resources\MessageBox\Error.png" />
<Resource Include="Resources\MessageBox\Information.png" />
<Resource Include="Resources\MessageBox\Question.png" />
<Resource Include="Resources\MessageBox\Warning.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Resources\Mods\OldCursor.png" /> <EmbeddedResource Include="Resources\Mods\Cursor\From2006\ArrowCursor.png" />
<EmbeddedResource Include="Resources\Mods\OldDeath.ogg" /> <EmbeddedResource Include="Resources\Mods\Cursor\From2006\ArrowFarCursor.png" />
<EmbeddedResource Include="Resources\Mods\OldFarCursor.png" /> <EmbeddedResource Include="Resources\Mods\Cursor\From2013\ArrowCursor.png" />
<EmbeddedResource Include="Resources\Mods\Cursor\From2013\ArrowFarCursor.png" />
<EmbeddedResource Include="Resources\Mods\Sounds\OldDeath.ogg" />
<EmbeddedResource Include="Resources\Mods\Sounds\OldGetUp.mp3" />
<EmbeddedResource Include="Resources\Mods\Sounds\OldJump.mp3" />
<EmbeddedResource Include="Resources\Mods\Sounds\OldWalk.mp3" />
<EmbeddedResource Include="Resources\Mods\Sounds\Empty.mp3" />
<EmbeddedResource Include="Resources\Mods\OldAvatarBackground.rbxl" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" /> <PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
<PackageReference Include="ini-parser-netstandard" Version="2.5.2" />
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" /> <PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
</ItemGroup> </ItemGroup>
@ -33,4 +47,13 @@
<ProjectReference Include="..\wpfui\src\Wpf.Ui\Wpf.Ui.csproj" /> <ProjectReference Include="..\wpfui\src\Wpf.Ui\Wpf.Ui.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="Bloxstrap.Models.Attributes.BuildMetadataAttribute">
<_Parameter1>$([System.DateTime]::UtcNow.ToString("s"))Z</_Parameter1>
<_Parameter2>$(COMPUTERNAME)\$(USERNAME)</_Parameter2>
<_Parameter3>$(CommitHash)</_Parameter3>
<_Parameter4>$(CommitRef)</_Parameter4>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

File diff suppressed because it is too large Load Diff

View File

@ -1,128 +0,0 @@
using System;
using System.Windows;
using System.Windows.Forms;
using Bloxstrap.Extensions;
namespace Bloxstrap.Dialogs
{
public class BootstrapperDialogForm : Form, IBootstrapperDialog
{
public Bootstrapper? Bootstrapper { get; set; }
#region UI Elements
protected virtual string _message { get; set; } = "Please wait...";
protected virtual ProgressBarStyle _progressStyle { get; set; }
protected virtual int _progressValue { get; set; }
protected virtual bool _cancelEnabled { get; set; }
public string Message
{
get => _message;
set
{
if (this.InvokeRequired)
this.Invoke(() => _message = value);
else
_message = value;
}
}
public ProgressBarStyle ProgressStyle
{
get => _progressStyle;
set
{
if (this.InvokeRequired)
this.Invoke(() => _progressStyle = value);
else
_progressStyle = value;
}
}
public int ProgressValue
{
get => _progressValue;
set
{
if (this.InvokeRequired)
this.Invoke(() => _progressValue = value);
else
_progressValue = value;
}
}
public bool CancelEnabled
{
get => _cancelEnabled;
set
{
if (this.InvokeRequired)
this.Invoke(() => _cancelEnabled = value);
else
_cancelEnabled = value;
}
}
#endregion
public void ScaleWindow()
{
this.Size = this.MinimumSize = this.MaximumSize = WindowScaling.GetScaledSize(this.Size);
foreach (Control control in this.Controls)
{
control.Size = WindowScaling.GetScaledSize(control.Size);
control.Location = WindowScaling.GetScaledPoint(control.Location);
control.Padding = WindowScaling.GetScaledPadding(control.Padding);
}
}
public void SetupDialog()
{
this.Text = App.Settings.Prop.BootstrapperTitle;
this.Icon = App.Settings.Prop.BootstrapperIcon.GetIcon();
}
public void ButtonCancel_Click(object? sender, EventArgs e)
{
Bootstrapper?.CancelInstall();
this.Close();
}
#region IBootstrapperDialog Methods
public void ShowBootstrapper() => this.ShowDialog();
public virtual void CloseBootstrapper()
{
if (this.InvokeRequired)
this.Invoke(CloseBootstrapper);
else
this.Close();
}
public virtual void ShowSuccess(string message)
{
App.ShowMessageBox(message, MessageBoxImage.Information);
App.Terminate();
}
public virtual void ShowError(string message)
{
App.ShowMessageBox($"An error occurred while starting Roblox\n\nDetails: {message}", MessageBoxImage.Error);
App.Terminate(Bootstrapper.ERROR_INSTALL_FAILURE);
}
public void PromptShutdown()
{
MessageBoxResult result = App.ShowMessageBox(
"Roblox is currently running, but needs to close. Would you like close Roblox now?",
MessageBoxImage.Information,
MessageBoxButton.OKCancel
);
if (result != MessageBoxResult.OK)
Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
}
#endregion
}
}

View File

@ -1,7 +1,4 @@
using System; namespace Bloxstrap
using System.IO;
namespace Bloxstrap
{ {
static class Directories static class Directories
{ {
@ -15,23 +12,27 @@ namespace Bloxstrap
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; } = "";
public static string Logs { get; private set; } = "";
public static string Integrations { get; private set; } = ""; public static string Integrations { get; private set; } = "";
public static string Versions { get; private set; } = ""; public static string Versions { get; private set; } = "";
public static string Modifications { get; private set; } = ""; public static string Modifications { get; private set; } = "";
public static string Application { get; private set; } = ""; public static string Application { get; private set; } = "";
public static bool Initialized => string.IsNullOrEmpty(Base); public static bool Initialized => !String.IsNullOrEmpty(Base);
public static void Initialize(string baseDirectory) public static void Initialize(string baseDirectory)
{ {
Base = baseDirectory; Base = baseDirectory;
Downloads = Path.Combine(Base, "Downloads"); Downloads = Path.Combine(Base, "Downloads");
Logs = Path.Combine(Base, "Logs");
Integrations = Path.Combine(Base, "Integrations"); Integrations = Path.Combine(Base, "Integrations");
Versions = Path.Combine(Base, "Versions"); Versions = Path.Combine(Base, "Versions");
Modifications = Path.Combine(Base, "Modifications"); Modifications = Path.Combine(Base, "Modifications");
Application = Path.Combine(Base, $"{App.ProjectName}.exe"); Application = Path.Combine(Base, $"{App.ProjectName}.exe");
} }
} }
} }

View File

@ -3,7 +3,7 @@
public enum BootstrapperIcon public enum BootstrapperIcon
{ {
IconBloxstrap, IconBloxstrap,
Icon2009, Icon2008,
Icon2011, Icon2011,
IconEarly2015, IconEarly2015,
IconLate2015, IconLate2015,

View File

@ -3,9 +3,10 @@
public enum BootstrapperStyle public enum BootstrapperStyle
{ {
VistaDialog, VistaDialog,
LegacyDialog2009, LegacyDialog2008,
LegacyDialog2011, LegacyDialog2011,
ProgressDialog, ProgressDialog,
FluentDialog FluentDialog,
ByfronDialog
} }
} }

View File

@ -0,0 +1,9 @@
namespace Bloxstrap.Enums
{
public enum CursorType
{
Default,
From2006,
From2013
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Enums
{
public enum EmojiType
{
Default,
Catmoji,
Windows11,
Windows10,
Windows8
}
}

View File

@ -0,0 +1,14 @@
namespace Bloxstrap.Enums
{
// https://learn.microsoft.com/en-us/windows/win32/msi/error-codes
// https://i-logic.com/serial/errorcodes.htm
// just the ones that we're interested in
public enum ErrorCode
{
ERROR_SUCCESS = 0,
ERROR_INSTALL_USEREXIT = 1602,
ERROR_INSTALL_FAILURE = 1603,
ERROR_CANCELLED = 1223
}
}

View File

@ -0,0 +1,9 @@
namespace Bloxstrap.Enums
{
public enum ServerType
{
Public,
Private,
Reserved
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class HttpResponseUnsuccessfulException : Exception
{
public HttpResponseMessage ResponseMessage { get; }
public HttpResponseUnsuccessfulException(HttpResponseMessage responseMessage) : base()
{
ResponseMessage = responseMessage;
}
}
}

View File

@ -1,7 +1,4 @@
using System; using System.Drawing;
using System.Drawing;
using Bloxstrap.Enums;
namespace Bloxstrap.Extensions namespace Bloxstrap.Extensions
{ {
@ -33,7 +30,7 @@ namespace Bloxstrap.Extensions
return icon switch return icon switch
{ {
BootstrapperIcon.IconBloxstrap => Properties.Resources.IconBloxstrap, BootstrapperIcon.IconBloxstrap => Properties.Resources.IconBloxstrap,
BootstrapperIcon.Icon2009 => Properties.Resources.Icon2009, BootstrapperIcon.Icon2008 => Properties.Resources.Icon2008,
BootstrapperIcon.Icon2011 => Properties.Resources.Icon2011, BootstrapperIcon.Icon2011 => Properties.Resources.Icon2011,
BootstrapperIcon.IconEarly2015 => Properties.Resources.IconEarly2015, BootstrapperIcon.IconEarly2015 => Properties.Resources.IconEarly2015,
BootstrapperIcon.IconLate2015 => Properties.Resources.IconLate2015, BootstrapperIcon.IconLate2015 => Properties.Resources.IconLate2015,

View File

@ -1,21 +1,7 @@
using Bloxstrap.Dialogs; namespace Bloxstrap.Extensions
using Bloxstrap.Enums;
namespace Bloxstrap.Extensions
{ {
static class BootstrapperStyleEx static class BootstrapperStyleEx
{ {
public static IBootstrapperDialog GetNew(this BootstrapperStyle bootstrapperStyle) public static IBootstrapperDialog GetNew(this BootstrapperStyle bootstrapperStyle) => Controls.GetBootstrapperDialog(bootstrapperStyle);
{
return bootstrapperStyle switch
{
BootstrapperStyle.VistaDialog => new VistaDialog(),
BootstrapperStyle.LegacyDialog2009 => new LegacyDialog2009(),
BootstrapperStyle.LegacyDialog2011 => new LegacyDialog2011(),
BootstrapperStyle.ProgressDialog => new ProgressDialog(),
BootstrapperStyle.FluentDialog => new FluentDialog(),
_ => new FluentDialog()
};
}
} }
} }

View File

@ -0,0 +1,12 @@
namespace Bloxstrap.Extensions
{
static class CursorTypeEx
{
public static IReadOnlyDictionary<string, CursorType> Selections => new Dictionary<string, CursorType>
{
{ "Default", CursorType.Default },
{ "2013 (Angular)", CursorType.From2013 },
{ "2006 (Cartoony)", CursorType.From2006 },
};
}
}

View File

@ -0,0 +1,10 @@
namespace Bloxstrap.Extensions
{
static class DateTimeEx
{
public static string ToFriendlyString(this DateTime dateTime)
{
return dateTime.ToString("dddd, d MMMM yyyy 'at' h:mm:ss tt", CultureInfo.InvariantCulture);
}
}
}

View File

@ -0,0 +1,40 @@
namespace Bloxstrap.Extensions
{
static class EmojiTypeEx
{
public static IReadOnlyDictionary<string, EmojiType> Selections => new Dictionary<string, EmojiType>
{
{ "Default (Twemoji)", EmojiType.Default },
{ "Catmoji", EmojiType.Catmoji },
{ "Windows 11", EmojiType.Windows11 },
{ "Windows 10", EmojiType.Windows10 },
{ "Windows 8", EmojiType.Windows8 },
};
public static IReadOnlyDictionary<EmojiType, string> Filenames => new Dictionary<EmojiType, string>
{
{ EmojiType.Catmoji, "Catmoji.ttf" },
{ EmojiType.Windows11, "Win1122H2SegoeUIEmoji.ttf" },
{ EmojiType.Windows10, "Win10April2018SegoeUIEmoji.ttf" },
{ EmojiType.Windows8, "Win8.1SegoeUIEmoji.ttf" },
};
public static IReadOnlyDictionary<EmojiType, string> Hashes => new Dictionary<EmojiType, string>
{
{ EmojiType.Catmoji, "98138f398a8cde897074dd2b8d53eca0" },
{ EmojiType.Windows11, "d50758427673578ddf6c9edcdbf367f5" },
{ EmojiType.Windows10, "d8a7eecbebf9dfdf622db8ccda63aff5" },
{ EmojiType.Windows8, "2b01c6caabbe95afc92aa63b9bf100f3" },
};
public static string GetHash(this EmojiType emojiType) => Hashes[emojiType];
public static string GetUrl(this EmojiType emojiType)
{
if (emojiType == EmojiType.Default)
return "";
return $"https://github.com/NikSavchenk0/rbxcustom-fontemojis/raw/8a552f4aaaecfa58d6bd9b0540e1ac16e81faadb/{Filenames[emojiType]}";
}
}
}

View File

@ -1,5 +1,4 @@
using System.Drawing; using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Media; using System.Windows.Media;

View File

@ -1,5 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Bloxstrap.Enums;
namespace Bloxstrap.Extensions namespace Bloxstrap.Extensions
{ {

View File

@ -0,0 +1,199 @@
using System.Windows.Input;
using System.Windows.Media.Animation;
namespace Bloxstrap
{
public class FastFlagManager : JsonManager<Dictionary<string, object>>
{
public override string FileLocation => Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json");
// this is the value of the 'FStringPartTexturePackTablePre2022' flag
public const string OldTexturesFlagValue = "{\"foil\":{\"ids\":[\"rbxassetid://7546645012\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"brick\":{\"ids\":[\"rbxassetid://7546650097\",\"rbxassetid://7546645118\"],\"color\":[204,201,200,232]},\"cobblestone\":{\"ids\":[\"rbxassetid://7546652947\",\"rbxassetid://7546645118\"],\"color\":[212,200,187,250]},\"concrete\":{\"ids\":[\"rbxassetid://7546653951\",\"rbxassetid://7546654144\"],\"color\":[208,208,208,255]},\"diamondplate\":{\"ids\":[\"rbxassetid://7547162198\",\"rbxassetid://7546645118\"],\"color\":[170,170,170,255]},\"fabric\":{\"ids\":[\"rbxassetid://7547101130\",\"rbxassetid://7546645118\"],\"color\":[105,104,102,244]},\"glass\":{\"ids\":[\"rbxassetid://7547304948\",\"rbxassetid://7546645118\"],\"color\":[254,254,254,7]},\"granite\":{\"ids\":[\"rbxassetid://7547164710\",\"rbxassetid://7546645118\"],\"color\":[113,113,113,255]},\"grass\":{\"ids\":[\"rbxassetid://7547169285\",\"rbxassetid://7546645118\"],\"color\":[165,165,159,255]},\"ice\":{\"ids\":[\"rbxassetid://7547171356\",\"rbxassetid://7546645118\"],\"color\":[255,255,255,255]},\"marble\":{\"ids\":[\"rbxassetid://7547177270\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"metal\":{\"ids\":[\"rbxassetid://7547288171\",\"rbxassetid://7546645118\"],\"color\":[199,199,199,255]},\"pebble\":{\"ids\":[\"rbxassetid://7547291361\",\"rbxassetid://7546645118\"],\"color\":[208,208,208,255]},\"corrodedmetal\":{\"ids\":[\"rbxassetid://7547184629\",\"rbxassetid://7546645118\"],\"color\":[159,119,95,200]},\"sand\":{\"ids\":[\"rbxassetid://7547295153\",\"rbxassetid://7546645118\"],\"color\":[220,220,220,255]},\"slate\":{\"ids\":[\"rbxassetid://7547298114\",\"rbxassetid://7547298323\"],\"color\":[193,193,193,255]},\"wood\":{\"ids\":[\"rbxassetid://7547303225\",\"rbxassetid://7547298786\"],\"color\":[227,227,227,255]},\"woodplanks\":{\"ids\":[\"rbxassetid://7547332968\",\"rbxassetid://7546645118\"],\"color\":[212,209,203,255]},\"asphalt\":{\"ids\":[\"rbxassetid://9873267379\",\"rbxassetid://9438410548\"],\"color\":[123,123,123,234]},\"basalt\":{\"ids\":[\"rbxassetid://9873270487\",\"rbxassetid://9438413638\"],\"color\":[154,154,153,238]},\"crackedlava\":{\"ids\":[\"rbxassetid://9438582231\",\"rbxassetid://9438453972\"],\"color\":[74,78,80,156]},\"glacier\":{\"ids\":[\"rbxassetid://9438851661\",\"rbxassetid://9438453972\"],\"color\":[226,229,229,243]},\"ground\":{\"ids\":[\"rbxassetid://9439044431\",\"rbxassetid://9438453972\"],\"color\":[114,114,112,240]},\"leafygrass\":{\"ids\":[\"rbxassetid://9873288083\",\"rbxassetid://9438453972\"],\"color\":[121,117,113,234]},\"limestone\":{\"ids\":[\"rbxassetid://9873289812\",\"rbxassetid://9438453972\"],\"color\":[235,234,230,250]},\"mud\":{\"ids\":[\"rbxassetid://9873319819\",\"rbxassetid://9438453972\"],\"color\":[130,130,130,252]},\"pavement\":{\"ids\":[\"rbxassetid://9873322398\",\"rbxassetid://9438453972\"],\"color\":[142,142,144,236]},\"rock\":{\"ids\":[\"rbxassetid://9873515198\",\"rbxassetid://9438453972\"],\"color\":[154,154,154,248]},\"salt\":{\"ids\":[\"rbxassetid://9439566986\",\"rbxassetid://9438453972\"],\"color\":[220,220,221,255]},\"sandstone\":{\"ids\":[\"rbxassetid://9873521380\",\"rbxassetid://9438453972\"],\"color\":[174,171,169,246]},\"snow\":{\"ids\":[\"rbxassetid://9439632387\",\"rbxassetid://9438453972\"],\"color\":[218,218,218,255]}}";
public static IReadOnlyDictionary<string, string> PresetFlags = new Dictionary<string, string>
{
{ "HTTP.Log", "DFLogHttpTraceLight" },
{ "HTTP.Proxy.Enable", "DFFlagDebugEnableHttpProxy" },
{ "HTTP.Proxy.Address.1", "DFStringDebugPlayerHttpProxyUrl" },
{ "HTTP.Proxy.Address.2", "DFStringHttpCurlProxyHostAndPort" },
{ "HTTP.Proxy.Address.3", "DFStringHttpCurlProxyHostAndPortForExternalUrl" },
{ "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" },
{ "Rendering.Fullscreen", "FFlagHandleAltEnterFullscreenManually" },
{ "Rendering.TexturePack", "FStringPartTexturePackTable2022" },
{ "Rendering.DPI.Disable", "DFFlagDisableDPIScale" },
{ "Rendering.DPI.Variable", "DFFlagVariableDPIScale2" },
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
{ "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" },
{ "Rendering.Mode.Vulkan.Fix", "FFlagRenderVulkanFixMinimizeWindow" },
{ "Rendering.Mode.OpenGL", "FFlagDebugGraphicsPreferOpenGL" },
{ "Rendering.Lighting.Voxel", "DFFlagDebugRenderForceTechnologyVoxel" },
{ "Rendering.Lighting.ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" },
{ "Rendering.Lighting.Future", "FFlagDebugForceFutureIsBrightPhase3" },
{ "UI.Hide", "DFIntCanHideGuiGroupId" },
{ "UI.FlagState", "FStringDebugShowFlagState" },
{ "UI.Menu.GraphicsSlider", "FFlagFixGraphicsQuality" },
{ "UI.Menu.Style.DisableV2", "FFlagDisableNewIGMinDUA" },
{ "UI.Menu.Style.EnableV4", "FFlagEnableInGameMenuControls" }
};
// only one missing here is Metal because lol
public static IReadOnlyDictionary<string, string> RenderingModes => new Dictionary<string, string>
{
{ "Automatic", "None" },
{ "Vulkan", "Vulkan" },
{ "Direct3D 11", "D3D11" },
{ "Direct3D 10", "D3D10" },
{ "OpenGL", "OpenGL" }
};
public static IReadOnlyDictionary<string, string> LightingModes => new Dictionary<string, string>
{
{ "Chosen by game", "None" },
{ "Voxel (Phase 1)", "Voxel" },
{ "ShadowMap (Phase 2)", "ShadowMap" },
{ "Future (Phase 3)", "Future" }
};
// this is one hell of a dictionary definition lmao
// since these all set the same flags, wouldn't making this use bitwise operators be better?
public static IReadOnlyDictionary<string, Dictionary<string, string?>> IGMenuVersions => new Dictionary<string, Dictionary<string, string?>>
{
{
"Default",
new Dictionary<string, string?>
{
{ "DisableV2", null },
{ "EnableV4", null }
}
},
{
"Version 1 (2015)",
new Dictionary<string, string?>
{
{ "DisableV2", "True" },
{ "EnableV4", "False" }
}
},
{
"Version 2 (2020)",
new Dictionary<string, string?>
{
{ "DisableV2", "False" },
{ "EnableV4", "False" }
}
},
{
"Version 4 (2023)",
new Dictionary<string, string?>
{
{ "DisableV2", "True" },
{ "EnableV4", "True" }
}
}
};
// all fflags are stored as strings
// to delete a flag, set the value as null
public void SetValue(string key, object? value)
{
if (value is null)
{
if (Prop.ContainsKey(key))
App.Logger.WriteLine($"[FastFlagManager::SetValue] Deletion of '{key}' is pending");
Prop.Remove(key);
}
else
{
if (Prop.ContainsKey(key))
App.Logger.WriteLine($"[FastFlagManager::SetValue] Setting of '{key}' from '{Prop[key]}' to '{value}' is pending");
else
App.Logger.WriteLine($"[FastFlagManager::SetValue] Setting of '{key}' to '{value}' is pending");
Prop[key] = value.ToString()!;
}
}
// this returns null if the fflag doesn't exist
public string? GetValue(string key)
{
// check if we have an updated change for it pushed first
if (Prop.TryGetValue(key, out object? value) && value is not null)
return value.ToString();
return null;
}
public void SetPreset(string prefix, object? value)
{
foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix)))
SetValue(pair.Value, value);
}
public void SetPresetOnce(string key, object? value)
{
if (GetPreset(key) is null)
SetPreset(key, value);
}
public void SetPresetEnum(string prefix, string target, object? value)
{
foreach (var pair in PresetFlags.Where(x => x.Key.StartsWith(prefix)))
{
if (pair.Key.StartsWith($"{prefix}.{target}"))
SetValue(pair.Value, value);
else
SetValue(pair.Value, null);
}
}
public string? GetPreset(string name) => GetValue(PresetFlags[name]);
public string GetPresetEnum(IReadOnlyDictionary<string, string> mapping, string prefix, string value)
{
foreach (var pair in mapping)
{
if (pair.Value == "None")
continue;
if (GetPreset($"{prefix}.{pair.Value}") == value)
return pair.Key;
}
return mapping.First().Key;
}
public override void Save()
{
// convert all flag values to strings before saving
foreach (var pair in Prop)
Prop[pair.Key] = pair.Value.ToString()!;
base.Save();
}
public override void Load()
{
base.Load();
SetPresetOnce("Rendering.Framerate", 9999);
SetPresetOnce("Rendering.Fullscreen", "False");
SetPresetOnce("Rendering.DPI.Disable", "True");
SetPresetOnce("Rendering.DPI.Variable", "False");
}
}
}

23
Bloxstrap/GlobalUsings.cs Normal file
View File

@ -0,0 +1,23 @@
global using System;
global using System.Collections.Generic;
global using System.Diagnostics;
global using System.Globalization;
global using System.IO;
global using System.IO.Compression;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
global using System.Linq;
global using System.Net;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
global using Bloxstrap.Enums;
global using Bloxstrap.Extensions;
global using Bloxstrap.Models;
global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.RobloxApi;
global using Bloxstrap.UI;
global using Bloxstrap.Utility;

View File

@ -0,0 +1,22 @@
namespace Bloxstrap
{
internal class HttpClientLoggingHandler : MessageProcessingHandler
{
public HttpClientLoggingHandler(HttpMessageHandler innerHandler)
: base(innerHandler)
{
}
protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
App.Logger.WriteLine($"[HttpClientLoggingHandler::HttpRequestMessage] {request.Method} {request.RequestUri}");
return request;
}
protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken)
{
App.Logger.WriteLine($"[HttpClientLoggingHandler::HttpResponseMessage] {(int)response.StatusCode} {response.ReasonPhrase} {response.RequestMessage!.RequestUri}");
return response;
}
}
}

View File

@ -1,11 +1,4 @@
using System; using DiscordRPC;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DiscordRPC;
using Bloxstrap.Models.RobloxApi;
namespace Bloxstrap.Integrations namespace Bloxstrap.Integrations
{ {
@ -14,6 +7,9 @@ namespace Bloxstrap.Integrations
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486"); private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
private readonly RobloxActivity _activityWatcher; private readonly RobloxActivity _activityWatcher;
private RichPresence? _currentPresence;
private bool _visible = true;
private string? _initialStatus;
private long _currentUniverseId; private long _currentUniverseId;
private DateTime? _timeStartedUniverse; private DateTime? _timeStartedUniverse;
@ -21,14 +17,15 @@ namespace Bloxstrap.Integrations
{ {
_activityWatcher = activityWatcher; _activityWatcher = activityWatcher;
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetPresence()); _activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetPresence()); _activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameMessage += (_, message) => OnGameMessage(message);
_rpcClient.OnReady += (_, e) => _rpcClient.OnReady += (_, e) =>
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})"); App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})");
_rpcClient.OnPresenceUpdate += (_, e) => _rpcClient.OnPresenceUpdate += (_, e) =>
App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Updated presence"); App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Presence updated");
_rpcClient.OnError += (_, e) => _rpcClient.OnError += (_, e) =>
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] An RPC error occurred - {e.Message}"); App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] An RPC error occurred - {e.Message}");
@ -46,83 +43,152 @@ namespace Bloxstrap.Integrations
_rpcClient.Initialize(); _rpcClient.Initialize();
} }
public async Task<bool> SetPresence() public void OnGameMessage(GameMessage message)
{
if (message.Command == "SetPresenceStatus")
SetStatus(message.Data);
}
public void SetStatus(string status)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Setting status to '{status}'");
if (_currentPresence is null)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Presence is not set, aborting");
return;
}
if (status.Length > 128)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status cannot be longer than 128 characters, aborting");
return;
}
if (_initialStatus is null)
_initialStatus = _currentPresence.State;
string finalStatus;
if (string.IsNullOrEmpty(status))
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status is empty, reverting to initial status");
finalStatus = _initialStatus;
}
else
{
finalStatus = status;
}
if (_currentPresence.State == finalStatus)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status is unchanged, aborting");
return;
}
_currentPresence.State = finalStatus;
UpdatePresence();
}
public void SetVisibility(bool visible)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetVisibility] Setting presence visibility ({visible})");
_visible = visible;
if (_visible)
UpdatePresence();
else
_rpcClient.ClearPresence();
}
public async Task<bool> SetCurrentGame()
{ {
if (!_activityWatcher.ActivityInGame) if (!_activityWatcher.ActivityInGame)
{ {
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Clearing presence"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Not in game, clearing presence");
_rpcClient.ClearPresence(); _currentPresence = null;
_initialStatus = null;
UpdatePresence();
return true; return true;
} }
string icon = "roblox"; string icon = "roblox";
long placeId = _activityWatcher.ActivityPlaceId;
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Setting presence for Place ID {_activityWatcher.ActivityPlaceId}"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Setting presence for Place ID {placeId}");
var universeIdResponse = await Utilities.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{_activityWatcher.ActivityPlaceId}/universe"); var universeIdResponse = await Http.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{placeId}/universe");
if (universeIdResponse is null) if (universeIdResponse is null)
{ {
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe ID!"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe ID!");
return false; return false;
} }
long universeId = universeIdResponse.UniverseId; long universeId = universeIdResponse.UniverseId;
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Got Universe ID as {universeId}"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe ID as {universeId}");
// preserve time spent playing if we're teleporting between places in the same universe // preserve time spent playing if we're teleporting between places in the same universe
if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId) if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId)
_timeStartedUniverse = DateTime.UtcNow; _timeStartedUniverse = DateTime.UtcNow;
_activityWatcher.ActivityIsTeleport = false;
_currentUniverseId = universeId; _currentUniverseId = universeId;
var gameDetailResponse = await Utilities.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeId}"); var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeId}");
if (gameDetailResponse is null || !gameDetailResponse.Data.Any()) if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
{ {
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe info!"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe info!");
return false; return false;
} }
GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0]; GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0];
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Got Universe details"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe details");
var universeThumbnailResponse = await Utilities.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false");
if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any()) if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any())
{ {
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe thumbnail info!"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe thumbnail info!");
} }
else else
{ {
icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl; icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl;
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Got Universe thumbnail as {icon}"); App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe thumbnail as {icon}");
} }
List<Button> buttons = new() List<Button> buttons = new();
{
new Button
{
Label = "See Details",
Url = $"https://www.roblox.com/games/{_activityWatcher.ActivityPlaceId}"
}
};
if (!App.Settings.Prop.HideRPCButtons) if (!App.Settings.Prop.HideRPCButtons && _activityWatcher.ActivityServerType == ServerType.Public)
{ {
buttons.Insert(0, new Button buttons.Add(new Button
{ {
Label = "Join", Label = "Join server",
Url = $"roblox://experiences/start?placeId={_activityWatcher.ActivityPlaceId}&gameInstanceId={_activityWatcher.ActivityJobId}" Url = $"roblox://experiences/start?placeId={placeId}&gameInstanceId={_activityWatcher.ActivityJobId}"
}); });
} }
// so turns out discord rejects the presence set request if the place name is less than 2 characters long lol buttons.Add(new Button
if (universeDetails.Name.Length < 2)
universeDetails.Name = $"{universeDetails.Name}\x2800\x2800\x2800";
_rpcClient.SetPresence(new RichPresence
{ {
Details = universeDetails.Name, Label = "See game page",
State = $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""), Url = $"https://www.roblox.com/games/{placeId}"
});
if (!_activityWatcher.ActivityInGame || placeId != _activityWatcher.ActivityPlaceId)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Aborting presence set because game activity has changed");
return false;
}
string status = _activityWatcher.ActivityServerType switch
{
ServerType.Private => "In a private server",
ServerType.Reserved => "In a reserved server",
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""),
};
_currentPresence = new RichPresence
{
Details = $"Playing {universeDetails.Name}",
State = status,
Timestamps = new Timestamps { Start = _timeStartedUniverse }, Timestamps = new Timestamps { Start = _timeStartedUniverse },
Buttons = buttons.ToArray(), Buttons = buttons.ToArray(),
Assets = new Assets Assets = new Assets
@ -132,16 +198,34 @@ namespace Bloxstrap.Integrations
SmallImageKey = "roblox", SmallImageKey = "roblox",
SmallImageText = "Roblox" SmallImageText = "Roblox"
} }
}); };
UpdatePresence();
return true; return true;
} }
public void UpdatePresence()
{
if (_currentPresence is null)
{
App.Logger.WriteLine($"[DiscordRichPresence::UpdatePresence] Presence is empty, clearing");
_rpcClient.ClearPresence();
return;
}
App.Logger.WriteLine($"[DiscordRichPresence::UpdatePresence] Updating presence");
if (_visible)
_rpcClient.SetPresence(_currentPresence);
}
public void Dispose() public void Dispose()
{ {
App.Logger.WriteLine("[DiscordRichPresence::Dispose] Cleaning up Discord RPC and Presence"); App.Logger.WriteLine("[DiscordRichPresence::Dispose] Cleaning up Discord RPC and Presence");
_rpcClient.ClearPresence(); _rpcClient.ClearPresence();
_rpcClient.Dispose(); _rpcClient.Dispose();
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -1,57 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Windows;
namespace Bloxstrap.Integrations
{
public class ServerNotifier
{
private readonly RobloxActivity _activityWatcher;
public ServerNotifier(RobloxActivity activityWatcher)
{
_activityWatcher = activityWatcher;
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => Notify());
}
public async void Notify()
{
string machineAddress = _activityWatcher.ActivityMachineAddress;
string message = "";
App.Logger.WriteLine($"[ServerNotifier::Notify] Getting server information for {machineAddress}");
// basically nobody has a free public access geolocation api that's accurate,
// the ones that do require an api key which isn't suitable for a client-side application like this
// so, hopefully this is reliable enough?
string locationCity = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{machineAddress}/city");
string locationRegion = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{machineAddress}/region");
string locationCountry = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{machineAddress}/country");
locationCity = locationCity.ReplaceLineEndings("");
locationRegion = locationRegion.ReplaceLineEndings("");
locationCountry = locationCountry.ReplaceLineEndings("");
if (String.IsNullOrEmpty(locationCountry))
message = "Location: N/A";
else if (locationCity == locationRegion)
message = $"Location: {locationRegion}, {locationCountry}";
else
message = $"Location: {locationCity}, {locationRegion}, {locationCountry}";
message += "\nClick to copy Job ID";
App.Logger.WriteLine($"[ServerNotifier::Notify] {message.ReplaceLineEndings("\\n")}");
EventHandler JobIDCopier = new((_, _) => Clipboard.SetText(_activityWatcher.ActivityJobId));
App.Notification.BalloonTipTitle = "Connected to server";
App.Notification.BalloonTipText = message;
App.Notification.BalloonTipClicked += JobIDCopier;
App.Notification.ShowBalloonTip(10);
await Task.Delay(10000);
App.Notification.BalloonTipClicked -= JobIDCopier;
}
}
}

View File

@ -1,8 +1,4 @@
using System; namespace Bloxstrap
using System.IO;
using System.Text.Json;
namespace Bloxstrap.Singletons
{ {
public class JsonManager<T> where T : new() public class JsonManager<T> where T : new()
{ {
@ -11,7 +7,7 @@ namespace Bloxstrap.Singletons
public virtual void Load() public virtual void Load()
{ {
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Loading JSON from {FileLocation}..."); App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Loading from {FileLocation}...");
try try
{ {
@ -22,28 +18,28 @@ namespace Bloxstrap.Singletons
Prop = settings; Prop = settings;
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] JSON loaded successfully!"); App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Loaded successfully!");
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Failed to load JSON! ({ex.Message})"); App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Failed to load! ({ex.Message})");
} }
} }
public virtual void Save() public virtual void Save()
{ {
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Attempting to save JSON to {FileLocation}...");
if (!App.ShouldSaveConfigs) if (!App.ShouldSaveConfigs)
{ {
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Aborted save (ShouldSave set to false)"); App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Save request ignored");
return; return;
} }
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Saving to {FileLocation}...");
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!); Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true })); File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] JSON saved!"); App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Save complete!");
} }
} }
} }

96
Bloxstrap/Logger.cs Normal file
View File

@ -0,0 +1,96 @@
namespace Bloxstrap
{
// https://stackoverflow.com/a/53873141/11852173
// TODO - this kind of sucks
// the main problem is just that this doesn't finish writing log entries before exiting the program
// this can be solved by making writetolog completely synchronous, but while it doesn't affect performance, its's not ideal
// also, writing and flushing for every single line that's written may not be great
public class Logger
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private FileStream? _filestream;
public readonly List<string> Backlog = new();
public bool Initialized = false;
public string? FileLocation;
public void Initialize(bool useTempDir = false)
{
string directory = useTempDir ? Path.Combine(Directories.LocalAppData, "Temp") : Path.Combine(Directories.Base, "Logs");
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
string filename = $"{App.ProjectName}_{timestamp}.log";
string location = Path.Combine(directory, filename);
WriteLine($"[Logger::Initialize] Initializing at {location}");
if (Initialized)
{
WriteLine("[Logger::Initialize] Failed to initialize because logger is already initialized");
return;
}
Directory.CreateDirectory(directory);
if (File.Exists(location))
{
WriteLine("[Logger::Initialize] Failed to initialize because log file already exists");
return;
}
_filestream = File.Open(location, FileMode.Create, FileAccess.Write, FileShare.Read);
Initialized = true;
if (Backlog.Count > 0)
WriteToLog(string.Join("\r\n", Backlog));
WriteLine($"[Logger::Initialize] Finished initializing!");
FileLocation = location;
// clean up any logs older than a week
if (Directories.Initialized && Directory.Exists(Directories.Logs))
{
foreach (FileInfo log in new DirectoryInfo(Directories.Logs).GetFiles())
{
if (log.LastWriteTimeUtc.AddDays(7) > DateTime.UtcNow)
continue;
App.Logger.WriteLine($"[Logger::Initialize] Cleaning up old log file '{log.Name}'");
log.Delete();
}
}
}
public void WriteLine(string message)
{
string timestamp = DateTime.UtcNow.ToString("s") + "Z";
string outcon = $"{timestamp} {message}";
string outlog = outcon.Replace(Directories.UserProfile, "%UserProfile%");
Debug.WriteLine(outcon);
WriteToLog(outlog);
}
private async void WriteToLog(string message)
{
if (!Initialized)
{
Backlog.Add(message);
return;
}
try
{
await _semaphore.WaitAsync();
await _filestream!.WriteAsync(Encoding.Unicode.GetBytes($"{message}\r\n"));
await _filestream.FlushAsync();
}
finally
{
_semaphore.Release();
}
}
}
}

View File

@ -0,0 +1,19 @@
namespace Bloxstrap.Models.Attributes
{
[AttributeUsage(AttributeTargets.Assembly)]
public class BuildMetadataAttribute : Attribute
{
public DateTime Timestamp { get; set; }
public string Machine { get; set; }
public string CommitHash { get; set; }
public string CommitRef { get; set; }
public BuildMetadataAttribute(string timestamp, string machine, string commitHash, string commitRef)
{
Timestamp = DateTime.Parse(timestamp).ToLocalTime();
Machine = machine;
CommitHash = commitHash;
CommitRef = commitRef;
}
}
}

View File

@ -1,7 +1,4 @@
using System; namespace Bloxstrap.Models
using System.Text.Json.Serialization;
namespace Bloxstrap.Models
{ {
public class ClientVersion public class ClientVersion
{ {
@ -15,5 +12,7 @@ namespace Bloxstrap.Models
public string BootstrapperVersion { get; set; } = null!; public string BootstrapperVersion { get; set; } = null!;
public DateTime? Timestamp { get; set; } public DateTime? Timestamp { get; set; }
public bool IsBehindDefaultChannel { get; set; }
} }
} }

View File

@ -1,10 +1,4 @@
using System; namespace Bloxstrap.Models
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Models
{ {
public class DeployInfo public class DeployInfo
{ {

View File

@ -0,0 +1,9 @@
namespace Bloxstrap.Models
{
public class FastFlag
{
// public bool Enabled { get; set; }
public string Name { get; set; } = null!;
public string Value { get; set; } = null!;
}
}

View File

@ -0,0 +1,17 @@
namespace Bloxstrap.Models
{
public class FontFace
{
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("weight")]
public int Weight { get; set; }
[JsonPropertyName("style")]
public string Style { get; set; } = null!;
[JsonPropertyName("assetId")]
public string AssetId { get; set; } = null!;
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models
{
public class FontFamily
{
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("faces")]
public IEnumerable<FontFace> Faces { get; set; } = null!;
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models
{
public class GameMessage
{
[JsonPropertyName("command")]
public string Command { get; set; } = null!;
[JsonPropertyName("data")]
public string Data { get; set; } = null!;
}
}

View File

@ -1,7 +1,4 @@
using System.Collections.Generic; namespace Bloxstrap.Models
using System.Text.Json.Serialization;
namespace Bloxstrap.Models
{ {
public class GithubRelease public class GithubRelease
{ {

View File

@ -4,11 +4,6 @@
* Copyright (c) 2015-present MaximumADHD * Copyright (c) 2015-present MaximumADHD
*/ */
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Bloxstrap.Models namespace Bloxstrap.Models
{ {
public class PackageManifest : List<Package> public class PackageManifest : List<Package>

View File

@ -1,7 +1,4 @@
using System.Collections.Generic; namespace Bloxstrap.Models.RobloxApi
using System.Text.Json.Serialization;
namespace Bloxstrap.Models.RobloxApi
{ {
/// <summary> /// <summary>
/// Roblox.Web.WebAPI.Models.ApiArrayResponse /// Roblox.Web.WebAPI.Models.ApiArrayResponse

View File

@ -1,6 +1,4 @@
using System.Text.Json.Serialization; namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.RobloxApi
{ {
/// <summary> /// <summary>
/// Roblox.Games.Api.Models.Response.GameCreator /// Roblox.Games.Api.Models.Response.GameCreator

View File

@ -1,8 +1,4 @@
using System; namespace Bloxstrap.Models.RobloxApi
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Bloxstrap.Models.RobloxApi
{ {
/// <summary> /// <summary>

View File

@ -1,6 +1,4 @@
using System.Text.Json.Serialization; namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.RobloxApi
{ {
/// <summary> /// <summary>
/// Roblox.Web.Responses.Thumbnails.ThumbnailResponse /// Roblox.Web.Responses.Thumbnails.ThumbnailResponse

View File

@ -1,6 +1,4 @@
using System.Text.Json.Serialization; namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.RobloxApi
{ {
// lmao its just one property // lmao its just one property
public class UniverseIdResponse public class UniverseIdResponse

View File

@ -1,7 +1,5 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Bloxstrap.Enums;
namespace Bloxstrap.Models namespace Bloxstrap.Models
{ {
public class Settings public class Settings
@ -15,12 +13,14 @@ namespace Bloxstrap.Models
public bool CheckForUpdates { get; set; } = true; public bool CheckForUpdates { get; set; } = true;
public bool CreateDesktopIcon { get; set; } = true; public bool CreateDesktopIcon { get; set; } = true;
public bool MultiInstanceLaunching { get; set; } = false; public bool MultiInstanceLaunching { get; set; } = false;
public bool OhHeyYouFoundMe { get; set; } = false;
// channel configuration // channel configuration
public string Channel { get; set; } = RobloxDeployment.DefaultChannel; public string Channel { get; set; } = RobloxDeployment.DefaultChannel;
public ChannelChangeMode ChannelChangeMode { get; set; } = ChannelChangeMode.Automatic; public ChannelChangeMode ChannelChangeMode { get; set; } = ChannelChangeMode.Automatic;
// integration configuration // integration configuration
public bool EnableActivityTracking { get; set; } = true;
public bool UseDiscordRichPresence { get; set; } = true; public bool UseDiscordRichPresence { get; set; } = true;
public bool HideRPCButtons { get; set; } = true; public bool HideRPCButtons { get; set; } = true;
public bool ShowServerDetails { get; set; } = false; public bool ShowServerDetails { get; set; } = false;
@ -28,8 +28,11 @@ namespace Bloxstrap.Models
// mod preset configuration // mod preset configuration
public bool UseOldDeathSound { get; set; } = true; public bool UseOldDeathSound { get; set; } = true;
public bool UseOldMouseCursor { get; set; } = false; public bool UseOldCharacterSounds { get; set; } = false;
public bool UseDisableAppPatch { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false;
public bool UseOldAvatarBackground { get; set; } = false;
public CursorType CursorType { get; set; } = CursorType.Default;
public EmojiType EmojiType { get; set; } = EmojiType.Default;
public bool DisableFullscreenOptimizations { get; set; } = false; public bool DisableFullscreenOptimizations { get; set; } = false;
} }
} }

View File

@ -1,9 +1,8 @@
using System.Collections.Generic; namespace Bloxstrap.Models
namespace Bloxstrap.Models
{ {
public class State public class State
{ {
public string LastEnrolledChannel { get; set; } = "";
public string VersionGuid { get; set; } = ""; public string VersionGuid { get; set; } = "";
public List<string> ModManifest { get; set; } = new(); public List<string> ModManifest { get; set; } = new();
} }

View File

@ -59,7 +59,7 @@ namespace Bloxstrap.Properties {
resourceCulture = value; resourceCulture = value;
} }
} }
/// <summary> /// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap. /// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary> /// </summary>
@ -115,11 +115,11 @@ namespace Bloxstrap.Properties {
/// <summary> /// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary> /// </summary>
internal static System.Drawing.Icon Icon2009 internal static System.Drawing.Icon Icon2008
{ {
get get
{ {
object obj = ResourceManager.GetObject("Icon2009", resourceCulture); object obj = ResourceManager.GetObject("Icon2008", resourceCulture);
return ((System.Drawing.Icon)(obj)); return ((System.Drawing.Icon)(obj));
} }
} }

View File

@ -130,8 +130,8 @@
<data name="DarkCancelButtonHover" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="DarkCancelButtonHover" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\DarkCancelButtonHover.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\DarkCancelButtonHover.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="Icon2009" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="Icon2008" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Icon2009.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\Icon2008.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="Icon2011" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="Icon2011" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Icon2011.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\Icon2011.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>

View File

@ -18,6 +18,10 @@
"Bloxstrap (Menu)": { "Bloxstrap (Menu)": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "-menu" "commandLineArgs": "-menu"
},
"Bloxstrap (Deeplink)": {
"commandName": "Project",
"commandLineArgs": "roblox://experiences/start?placeId=95206881"
} }
} }
} }

View File

@ -1,13 +1,8 @@
using System; using System.Web;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Windows; using System.Windows;
using Microsoft.Win32; using Microsoft.Win32;
using Bloxstrap.Enums;
namespace Bloxstrap namespace Bloxstrap
{ {
static class ProtocolHandler static class ProtocolHandler
@ -31,6 +26,8 @@ namespace Bloxstrap
string[] keyvalPair; string[] keyvalPair;
string key; string key;
string val; string val;
bool channelArgPresent = false;
StringBuilder commandLine = new(); StringBuilder commandLine = new();
foreach (var parameter in protocol.Split('+')) foreach (var parameter in protocol.Split('+'))
@ -55,23 +52,10 @@ namespace Bloxstrap
if (key == "launchtime") if (key == "launchtime")
val = "LAUNCHTIMEPLACEHOLDER"; val = "LAUNCHTIMEPLACEHOLDER";
if (key == "channel") if (key == "channel" && !String.IsNullOrEmpty(val))
{ {
if (val.ToLower() != App.Settings.Prop.Channel.ToLower() && App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Ignore) channelArgPresent = true;
{ EnrollChannel(val);
MessageBoxResult result = App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Automatic ? MessageBoxResult.Yes : App.ShowMessageBox(
$"{App.ProjectName} was launched with the Roblox build channel set to {val}, however your current preferred channel is {App.Settings.Prop.Channel}.\n\n" +
$"Would you like to switch channels from {App.Settings.Prop.Channel} to {val}?",
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
if (result == MessageBoxResult.Yes)
{
App.Logger.WriteLine($"[Protocol::ParseUri] Changed Roblox build channel from {App.Settings.Prop.Channel} to {val}");
App.Settings.Prop.Channel = val;
}
}
// we'll set the arg when launching // we'll set the arg when launching
continue; continue;
@ -80,9 +64,47 @@ namespace Bloxstrap
commandLine.Append(UriKeyArgMap[key] + val + " "); commandLine.Append(UriKeyArgMap[key] + val + " ");
} }
if (!channelArgPresent)
EnrollChannel(RobloxDeployment.DefaultChannel);
return commandLine.ToString(); return commandLine.ToString();
} }
public static void ChangeChannel(string channel)
{
if (channel.ToLowerInvariant() == App.Settings.Prop.Channel.ToLowerInvariant())
return;
if (App.Settings.Prop.ChannelChangeMode == ChannelChangeMode.Ignore)
return;
if (App.Settings.Prop.ChannelChangeMode != ChannelChangeMode.Automatic)
{
if (channel == App.State.Prop.LastEnrolledChannel)
return;
MessageBoxResult result = Controls.ShowMessageBox(
$"Roblox is attempting to set your channel to {channel}, however your current preferred channel is {App.Settings.Prop.Channel}.\n\n" +
$"Would you like to switch your preferred channel to {channel}?",
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
if (result != MessageBoxResult.Yes)
return;
}
App.Logger.WriteLine($"[Protocol::ParseUri] Changed Roblox build channel from {App.Settings.Prop.Channel} to {channel}");
App.Settings.Prop.Channel = channel;
}
public static void EnrollChannel(string channel)
{
ChangeChannel(channel);
App.State.Prop.LastEnrolledChannel = channel;
App.State.Save();
}
public static void Register(string key, string name, string handler) public static void Register(string key, string name, string handler)
{ {
string handlerArgs = $"\"{handler}\" %1"; string handlerArgs = $"\"{handler}\" %1";

View File

@ -1,7 +1,4 @@
using System.IO; using System.Reflection;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Bloxstrap namespace Bloxstrap
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 232 B

View File

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,40 +1,44 @@
using System; namespace Bloxstrap
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Bloxstrap
{ {
public class RobloxActivity : IDisposable public class RobloxActivity : IDisposable
{ {
// i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence, // i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence,
// like checking the ping and region of the current connected server. maybe that's something to add? // like checking the ping and region of the current connected server. maybe that's something to add?
private const string GameJoiningEntry = "[FLog::Output] ! Joining game"; private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = "; private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
private const string GameJoinedEntry = "[FLog::Network] serverId:"; private const string GameJoinedEntry = "[FLog::Network] serverId:";
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:"; private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport"; private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
private const string GameMessageEntry = "[FLog::Output] [SendBloxstrapMessage]";
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+"; private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+"; private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
private int _logEntriesRead = 0; private int _logEntriesRead = 0;
private bool _teleportMarker = false;
private bool _reservedTeleportMarker = false;
public event EventHandler<string>? OnLogEntry;
public event EventHandler? OnGameJoin; public event EventHandler? OnGameJoin;
public event EventHandler? OnGameLeave; public event EventHandler? OnGameLeave;
public event EventHandler<GameMessage>? OnGameMessage;
private Dictionary<string, string> GeolcationCache = new();
public string LogLocation = null!;
// these are values to use assuming the player isn't currently in a game // these are values to use assuming the player isn't currently in a game
// keep in mind ActivityIsTeleport is only reset by DiscordRichPresence when it's done accessing it // hmm... do i move this to a model?
// because of the weird chronology of where the teleporting entry is outputted, there's no way to reset it in here
public bool ActivityInGame = false; public bool ActivityInGame = false;
public long ActivityPlaceId = 0; public long ActivityPlaceId = 0;
public string ActivityJobId = ""; public string ActivityJobId = "";
public string ActivityMachineAddress = ""; public string ActivityMachineAddress = "";
public bool ActivityMachineUDMUX = false; public bool ActivityMachineUDMUX = false;
public bool ActivityIsTeleport = false; public bool ActivityIsTeleport = false;
public ServerType ActivityServerType = ServerType.Public;
public bool IsDisposed = false; public bool IsDisposed = false;
@ -51,6 +55,11 @@ namespace Bloxstrap
// //
// we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity // we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity
int delay = 1000;
if (App.Settings.Prop.OhHeyYouFoundMe)
delay = 250;
string logDirectory = Path.Combine(Directories.LocalAppData, "Roblox\\logs"); string logDirectory = Path.Combine(Directories.LocalAppData, "Roblox\\logs");
if (!Directory.Exists(logDirectory)) if (!Directory.Exists(logDirectory))
@ -66,7 +75,11 @@ namespace Bloxstrap
while (true) while (true)
{ {
logFileInfo = new DirectoryInfo(logDirectory).GetFiles().OrderByDescending(x => x.CreationTime).First(); logFileInfo = new DirectoryInfo(logDirectory)
.GetFiles()
.Where(x => x.CreationTime <= DateTime.Now)
.OrderByDescending(x => x.CreationTime)
.First();
if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now) if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now)
break; break;
@ -75,8 +88,9 @@ namespace Bloxstrap
await Task.Delay(1000); await Task.Delay(1000);
} }
LogLocation = logFileInfo.FullName;
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
App.Logger.WriteLine($"[RobloxActivity::StartWatcher] Opened {logFileInfo.Name}"); App.Logger.WriteLine($"[RobloxActivity::StartWatcher] Opened {LogLocation}");
AutoResetEvent logUpdatedEvent = new(false); AutoResetEvent logUpdatedEvent = new(false);
FileSystemWatcher logWatcher = new() FileSystemWatcher logWatcher = new()
@ -94,7 +108,7 @@ namespace Bloxstrap
string? log = await sr.ReadLineAsync(); string? log = await sr.ReadLineAsync();
if (string.IsNullOrEmpty(log)) if (string.IsNullOrEmpty(log))
logUpdatedEvent.WaitOne(1000); logUpdatedEvent.WaitOne(delay);
else else
ExamineLogEntry(log); ExamineLogEntry(log);
} }
@ -102,7 +116,8 @@ namespace Bloxstrap
private void ExamineLogEntry(string entry) private void ExamineLogEntry(string entry)
{ {
// App.Logger.WriteLine(entry); OnLogEntry?.Invoke(this, entry);
_logEntriesRead += 1; _logEntriesRead += 1;
// debug stats to ensure that the log reader is working correctly // debug stats to ensure that the log reader is working correctly
@ -112,23 +127,43 @@ namespace Bloxstrap
else if (_logEntriesRead % 100 == 0) else if (_logEntriesRead % 100 == 0)
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Read {_logEntriesRead} log entries"); App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Read {_logEntriesRead} log entries");
if (!ActivityInGame && ActivityPlaceId == 0 && entry.Contains(GameJoiningEntry)) if (!ActivityInGame && ActivityPlaceId == 0)
{ {
Match match = Regex.Match(entry, GameJoiningEntryPattern); if (entry.Contains(GameJoiningPrivateServerEntry))
if (match.Groups.Count != 4)
{ {
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game join entry"); // we only expect to be joining a private server if we're not already in a game
App.Logger.WriteLine(entry); ActivityServerType = ServerType.Private;
return;
} }
else if (entry.Contains(GameJoiningEntry))
{
Match match = Regex.Match(entry, GameJoiningEntryPattern);
ActivityInGame = false; if (match.Groups.Count != 4)
ActivityPlaceId = long.Parse(match.Groups[2].Value); {
ActivityJobId = match.Groups[1].Value; App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game join entry");
ActivityMachineAddress = match.Groups[3].Value; App.Logger.WriteLine(entry);
return;
}
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); ActivityInGame = false;
ActivityPlaceId = long.Parse(match.Groups[2].Value);
ActivityJobId = match.Groups[1].Value;
ActivityMachineAddress = match.Groups[3].Value;
if (_teleportMarker)
{
ActivityIsTeleport = true;
_teleportMarker = false;
}
if (_reservedTeleportMarker)
{
ActivityServerType = ServerType.Reserved;
_reservedTeleportMarker = false;
}
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
}
} }
else if (!ActivityInGame && ActivityPlaceId != 0) else if (!ActivityInGame && ActivityPlaceId != 0)
{ {
@ -176,20 +211,86 @@ namespace Bloxstrap
ActivityJobId = ""; ActivityJobId = "";
ActivityMachineAddress = ""; ActivityMachineAddress = "";
ActivityMachineUDMUX = false; ActivityMachineUDMUX = false;
ActivityIsTeleport = false;
ActivityServerType = ServerType.Public;
OnGameLeave?.Invoke(this, new EventArgs()); OnGameLeave?.Invoke(this, new EventArgs());
} }
else if (entry.Contains(GameTeleportingEntry)) else if (entry.Contains(GameTeleportingEntry))
{ {
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
ActivityIsTeleport = true; _teleportMarker = true;
}
else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry))
{
// we only expect to be joining a reserved server if we're teleporting to one from a game
_reservedTeleportMarker = true;
}
else if (entry.Contains(GameMessageEntry))
{
string messagePlain = entry.Substring(entry.IndexOf(GameMessageEntry) + GameMessageEntry.Length + 1);
GameMessage? message;
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Received message: '{messagePlain}'");
try
{
message = JsonSerializer.Deserialize<GameMessage>(messagePlain);
}
catch (Exception)
{
App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (JSON deserialization threw an exception)");
return;
}
if (message is null)
{
App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (JSON deserialization returned null)");
return;
}
if (String.IsNullOrEmpty(message.Command))
{
App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (Command is empty)");
return;
}
OnGameMessage?.Invoke(this, message);
} }
} }
} }
public async Task<string> GetServerLocation()
{
if (GeolcationCache.ContainsKey(ActivityMachineAddress))
return GeolcationCache[ActivityMachineAddress];
string location = "";
string locationCity = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/city");
string locationRegion = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/region");
string locationCountry = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/country");
locationCity = locationCity.ReplaceLineEndings("");
locationRegion = locationRegion.ReplaceLineEndings("");
locationCountry = locationCountry.ReplaceLineEndings("");
if (String.IsNullOrEmpty(locationCountry))
location = "N/A";
else if (locationCity == locationRegion)
location = $"{locationRegion}, {locationCountry}";
else
location = $"{locationCity}, {locationRegion}, {locationCountry}";
GeolcationCache[ActivityMachineAddress] = location;
return location;
}
public void Dispose() public void Dispose()
{ {
IsDisposed = true; IsDisposed = true;
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -1,11 +1,4 @@
using System; using Bloxstrap.Exceptions;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Bloxstrap.Models;
namespace Bloxstrap namespace Bloxstrap
{ {
@ -14,6 +7,8 @@ namespace Bloxstrap
#region Properties #region Properties
public const string DefaultChannel = "LIVE"; public const string DefaultChannel = "LIVE";
private static Dictionary<string, ClientVersion> ClientVersionCache = new();
// a list of roblox delpoyment locations that we check for, in case one of them don't work // a list of roblox delpoyment locations that we check for, in case one of them don't work
private static List<string> BaseUrls = new() private static List<string> BaseUrls = new()
{ {
@ -78,46 +73,55 @@ namespace Bloxstrap
string location = BaseUrl; string location = BaseUrl;
if (channel.ToLower() != DefaultChannel.ToLower()) if (channel.ToLowerInvariant() != DefaultChannel.ToLowerInvariant())
location += $"/channel/{channel.ToLower()}"; location += $"/channel/{channel.ToLowerInvariant()}";
location += resource; location += resource;
return location; return location;
} }
public static async Task<ClientVersion> GetInfo(string channel, bool timestamp = false) public static async Task<ClientVersion> GetInfo(string channel, bool extraInformation = false)
{ {
App.Logger.WriteLine($"[RobloxDeployment::GetInfo] Getting deploy info for channel {channel} (timestamp={timestamp})"); App.Logger.WriteLine($"[RobloxDeployment::GetInfo] Getting deploy info for channel {channel} (extraInformation={extraInformation})");
HttpResponseMessage deployInfoResponse = await App.HttpClient.GetAsync($"https://clientsettingscdn.roblox.com/v2/client-version/WindowsPlayer/channel/{channel}"); ClientVersion clientVersion;
string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync(); if (ClientVersionCache.ContainsKey(channel))
if (!deployInfoResponse.IsSuccessStatusCode)
{ {
// 400 = Invalid binaryType. App.Logger.WriteLine($"[RobloxDeployment::GetInfo] Deploy information is cached");
// 404 = Could not find version details for binaryType. clientVersion = ClientVersionCache[channel];
// 500 = Error while fetching version information.
// either way, we throw
App.Logger.WriteLine(
"[RobloxDeployment::GetInfo] Failed to fetch deploy info!\r\n" +
$"\tStatus code: {deployInfoResponse.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new Exception($"Could not get latest deploy for channel {channel}! (HTTP {deployInfoResponse.StatusCode})");
} }
else
{
HttpResponseMessage deployInfoResponse = await App.HttpClient.GetAsync($"https://clientsettingscdn.roblox.com/v2/client-version/WindowsPlayer/channel/{channel}");
App.Logger.WriteLine($"[RobloxDeployment::GetInfo] Got JSON: {rawResponse}"); string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync();
ClientVersion clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawResponse)!; if (!deployInfoResponse.IsSuccessStatusCode)
{
// 400 = Invalid binaryType.
// 404 = Could not find version details for binaryType.
// 500 = Error while fetching version information.
// either way, we throw
App.Logger.WriteLine(
"[RobloxDeployment::GetInfo] Failed to fetch deploy info!\r\n" +
$"\tStatus code: {deployInfoResponse.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseUnsuccessfulException(deployInfoResponse);
}
clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawResponse)!;
}
// for preferences // for preferences
if (timestamp) if (extraInformation && clientVersion.Timestamp is null)
{ {
App.Logger.WriteLine("[RobloxDeployment::GetInfo] Getting timestamp..."); App.Logger.WriteLine("[RobloxDeployment::GetInfo] Getting extra information...");
string manifestUrl = GetLocation($"/{clientVersion.VersionGuid}-rbxPkgManifest.txt", channel); string manifestUrl = GetLocation($"/{clientVersion.VersionGuid}-rbxPkgManifest.txt", channel);
@ -130,8 +134,19 @@ namespace Bloxstrap
App.Logger.WriteLine($"[RobloxDeployment::GetInfo] {manifestUrl} - Last-Modified: {lastModified}"); App.Logger.WriteLine($"[RobloxDeployment::GetInfo] {manifestUrl} - Last-Modified: {lastModified}");
clientVersion.Timestamp = DateTime.Parse(lastModified).ToLocalTime(); clientVersion.Timestamp = DateTime.Parse(lastModified).ToLocalTime();
} }
// check if channel is behind LIVE
if (channel != DefaultChannel)
{
var defaultClientVersion = await GetInfo(DefaultChannel);
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == -1)
clientVersion.IsBehindDefaultChannel = true;
}
} }
ClientVersionCache[channel] = clientVersion;
return clientVersion; return clientVersion;
} }
} }

View File

@ -1,162 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace Bloxstrap.Singletons
{
public class FastFlagManager : JsonManager<Dictionary<string, object>>
{
public override string FileLocation => Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json");
// we put any changes we want to make to fastflags here
// these will apply after bloxstrap finishes installing or after the menu closes
// to delete a fastflag, set the value to null
public Dictionary<string, object?> Changes = new();
// only one missing here is Metal because lol
public static IReadOnlyDictionary<string, string> RenderingModes => new Dictionary<string, string>
{
{ "Automatic", "" },
{ "Direct3D 11", "FFlagDebugGraphicsPreferD3D11" },
{ "Vulkan", "FFlagDebugGraphicsPreferVulkan" },
{ "OpenGL", "FFlagDebugGraphicsPreferOpenGL" }
};
public static IReadOnlyDictionary<string, string> LightingTechnologies => new Dictionary<string, string>
{
{ "Automatic", "" },
{ "Voxel", "DFFlagDebugRenderForceTechnologyVoxel" },
{ "Future Is Bright", "FFlagDebugForceFutureIsBrightPhase3" }
};
// this is one hell of a variable definition lmao
public static IReadOnlyDictionary<string, Dictionary<string, string?>> IGMenuVersions => new Dictionary<string, Dictionary<string, string?>>
{
{
"Default",
new Dictionary<string, string?>
{
{ "FFlagDisableNewIGMinDUA", null },
{ "FFlagEnableInGameMenuV3", null }
}
},
{
"Version 1 (2015)",
new Dictionary<string, string?>
{
{ "FFlagDisableNewIGMinDUA", "True" },
{ "FFlagEnableInGameMenuV3", "False" }
}
},
{
"Version 2 (2020)",
new Dictionary<string, string?>
{
{ "FFlagDisableNewIGMinDUA", "False" },
{ "FFlagEnableInGameMenuV3", "False" }
}
},
{
"Version 3 (2021)",
new Dictionary<string, string?>
{
{ "FFlagDisableNewIGMinDUA", "False" },
{ "FFlagEnableInGameMenuV3", "True" }
}
}
};
// all fflags are stored as strings
// to delete a flag, set the value as null
public void SetValue(string key, object? value)
{
if (value is null)
{
Changes[key] = null;
App.Logger.WriteLine($"[FastFlagManager::SetValue] Deletion of '{key}' is pending");
}
else
{
Changes[key] = value.ToString();
App.Logger.WriteLine($"[FastFlagManager::SetValue] Value change for '{key}' to '{value}' is pending");
}
}
// this returns null if the fflag doesn't exist
public string? GetValue(string key)
{
// check if we have an updated change for it pushed first
if (Changes.TryGetValue(key, out object? changedValue))
return changedValue?.ToString();
if (Prop.TryGetValue(key, out object? value) && value is not null)
return value.ToString();
return null;
}
public void SetRenderingMode(string value)
{
foreach (var mode in RenderingModes)
{
if (mode.Key != "Automatic")
SetValue(mode.Value, null);
}
if (value != "Automatic")
SetValue(RenderingModes[value], "True");
}
public override void Load()
{
base.Load();
// set to 9999 by default if it doesnt already exist
if (GetValue("DFIntTaskSchedulerTargetFps") is null)
SetValue("DFIntTaskSchedulerTargetFps", 9999);
// reshade / exclusive fullscreen requires direct3d 11 to work
if (GetValue(RenderingModes["Direct3D 11"]) != "True" && App.FastFlags.GetValue("FFlagHandleAltEnterFullscreenManually") == "False")
SetRenderingMode("Direct3D 11");
}
public override void Save()
{
App.Logger.WriteLine($"[FastFlagManager::Save] Attempting to save JSON to {FileLocation}...");
// reload for any changes made while the menu was open
Load();
if (Changes.Count == 0)
{
App.Logger.WriteLine($"[FastFlagManager::Save] No changes to apply, aborting.");
return;
}
foreach (var change in Changes)
{
if (change.Value is null)
{
App.Logger.WriteLine($"[FastFlagManager::Save] Removing '{change.Key}'");
Prop.Remove(change.Key);
continue;
}
App.Logger.WriteLine($"[FastFlagManager::Save] Setting '{change.Key}' to '{change.Value}'");
Prop[change.Key] = change.Value;
}
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
Changes.Clear();
App.Logger.WriteLine($"[FastFlagManager::Save] JSON saved!");
}
}
}

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
namespace Bloxstrap.Singletons
{
// https://stackoverflow.com/a/53873141/11852173
public class Logger
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly List<string> _backlog = new();
private FileStream? _filestream;
public void Initialize(string filename)
{
if (_filestream is not null)
throw new Exception("Logger is already initialized");
string? directory = Path.GetDirectoryName(filename);
if (directory is not null)
Directory.CreateDirectory(directory);
_filestream = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.Read);
if (_backlog.Count > 0)
WriteToLog(string.Join("\r\n", _backlog));
WriteLine($"[Logger::Logger] Initialized at {filename}");
}
public void WriteLine(string message)
{
string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
string outcon = $"{timestamp} {message}";
string outlog = outcon.Replace(Directories.UserProfile, "<UserProfileFolder>");
Debug.WriteLine(outcon);
WriteToLog(outlog);
}
private async void WriteToLog(string message)
{
if (_filestream is null)
{
_backlog.Add(message);
return;
}
try
{
await _semaphore.WaitAsync();
await _filestream.WriteAsync(Encoding.Unicode.GetBytes($"{message}\r\n"));
await _filestream.FlushAsync();
}
finally
{
_semaphore.Release();
}
}
}
}

56
Bloxstrap/UI/Controls.cs Normal file
View File

@ -0,0 +1,56 @@
using System.Windows;
using Bloxstrap.UI.Elements.Bootstrapper;
using Bloxstrap.UI.Elements.Dialogs;
using Bloxstrap.UI.Elements.Menu;
namespace Bloxstrap.UI
{
static class Controls
{
public static void ShowMenu() => new MainWindow().ShowDialog();
public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.None)
{
if (App.IsQuiet)
return defaultResult;
switch (App.Settings.Prop.BootstrapperStyle)
{
case BootstrapperStyle.FluentDialog:
case BootstrapperStyle.ByfronDialog:
return Application.Current.Dispatcher.Invoke(new Func<MessageBoxResult>(() =>
{
var messagebox = new FluentMessageBox(message, icon, buttons);
messagebox.ShowDialog();
return messagebox.Result;
}));
default:
return System.Windows.MessageBox.Show(message, App.ProjectName, buttons, icon);
}
}
public static void ShowExceptionDialog(Exception exception)
{
Application.Current.Dispatcher.Invoke(() =>
{
new ExceptionDialog(exception).ShowDialog();
});
}
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
{
return style switch
{
BootstrapperStyle.VistaDialog => new VistaDialog(),
BootstrapperStyle.LegacyDialog2008 => new LegacyDialog2008(),
BootstrapperStyle.LegacyDialog2011 => new LegacyDialog2011(),
BootstrapperStyle.ProgressDialog => new ProgressDialog(),
BootstrapperStyle.FluentDialog => new FluentDialog(),
BootstrapperStyle.ByfronDialog => new ByfronDialog(),
_ => new FluentDialog()
};
}
}
}

View File

@ -0,0 +1,17 @@
using System.Windows;
namespace Bloxstrap.UI.Elements.Bootstrapper.Base
{
static class BaseFunctions
{
public static void ShowSuccess(string message, Action? callback)
{
Controls.ShowMessageBox(message, MessageBoxImage.Information);
if (callback is not null)
callback();
App.Terminate();
}
}
}

View File

@ -0,0 +1,104 @@
using System.Windows.Forms;
using Bloxstrap.UI.Utility;
namespace Bloxstrap.UI.Elements.Bootstrapper.Base
{
public class WinFormsDialogBase : Form, IBootstrapperDialog
{
public Bloxstrap.Bootstrapper? Bootstrapper { get; set; }
#region UI Elements
protected virtual string _message { get; set; } = "Please wait...";
protected virtual ProgressBarStyle _progressStyle { get; set; }
protected virtual int _progressValue { get; set; }
protected virtual bool _cancelEnabled { get; set; }
public string Message
{
get => _message;
set
{
if (InvokeRequired)
Invoke(() => _message = value);
else
_message = value;
}
}
public ProgressBarStyle ProgressStyle
{
get => _progressStyle;
set
{
if (InvokeRequired)
Invoke(() => _progressStyle = value);
else
_progressStyle = value;
}
}
public int ProgressValue
{
get => _progressValue;
set
{
if (InvokeRequired)
Invoke(() => _progressValue = value);
else
_progressValue = value;
}
}
public bool CancelEnabled
{
get => _cancelEnabled;
set
{
if (InvokeRequired)
Invoke(() => _cancelEnabled = value);
else
_cancelEnabled = value;
}
}
#endregion
public void ScaleWindow()
{
Size = MinimumSize = MaximumSize = WindowScaling.GetScaledSize(Size);
foreach (Control control in Controls)
{
control.Size = WindowScaling.GetScaledSize(control.Size);
control.Location = WindowScaling.GetScaledPoint(control.Location);
control.Padding = WindowScaling.GetScaledPadding(control.Padding);
}
}
public void SetupDialog()
{
Text = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon();
}
public void ButtonCancel_Click(object? sender, EventArgs e)
{
Bootstrapper?.CancelInstall();
Close();
}
#region IBootstrapperDialog Methods
public void ShowBootstrapper() => ShowDialog();
public virtual void CloseBootstrapper()
{
if (InvokeRequired)
Invoke(CloseBootstrapper);
else
Close();
}
public virtual void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback);
#endregion
}
}

View File

@ -0,0 +1,44 @@
<Window x:Class="Bloxstrap.UI.Elements.Bootstrapper.ByfronDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Width="600"
Height="400"
ResizeMode="NoResize"
WindowStyle="None"
WindowStartupLocation="CenterScreen"
AllowsTransparency="True"
Background="Transparent">
<Border CornerRadius="10" BorderBrush="#33393B3D" Background="{Binding Background}" BorderThickness="{Binding DialogBorder}">
<Grid>
<Image Source="{Binding ByfronLogoLocation}" Width="114" Height="108" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="17,13,0,0" />
<Button Margin="15" VerticalAlignment="Top" HorizontalAlignment="Right" Width="20" Height="20" BorderThickness="0" Padding="1" Background="Transparent" BorderBrush="Transparent" Visibility="{Binding CancelButtonVisibility, Mode=OneWay}" Command="{Binding CancelInstallCommand}">
<Path Fill="{Binding IconColor}" Stretch="Fill">
<Path.Data>
<PathGeometry Figures="m 6.7507011 2.1168752 c -1.0144874 0 -2.0291944 0.3884677 -2.8065471 1.1658203 -1.5547052 1.5547053 -1.5547052 4.0583888 10e-8 5.6130941 L 28.499761 33.45088 3.9441541 58.006487 c -1.5547058 1.554706 -1.5547051 4.057873 0 5.612578 1.554705 1.554705 4.0583891 1.554705 5.6130942 0 l 24.5550907 -24.55509 24.555607 24.55509 c 1.554705 1.554705 4.057871 1.554705 5.612577 0 1.554705 -1.554706 1.554705 -4.057872 0 -5.612577 L 39.725433 33.450881 64.280523 8.89579 c 1.554706 -1.5547058 1.554705 -4.0583892 0 -5.6130942 -1.554705 -1.5547051 -4.057872 -1.5547058 -5.612578 -7e-7 L 34.112338 27.838303 9.5572482 3.2826955 C 8.7798955 2.5053428 7.7651883 2.1168752 6.7507011 2.1168752 Z" FillRule="NonZero"/>
</Path.Data>
</Path>
</Button>
<TextBlock Margin="15" VerticalAlignment="Top" HorizontalAlignment="Right" FontSize="10" FontFamily="{StaticResource Rubik}" Text="{Binding VersionText}" Foreground="{Binding Foreground}" Visibility="{Binding VersionTextVisibility, Mode=OneWay}" />
<Path Fill="{Binding IconColor}" Width="300" Height="56" Stretch="Fill">
<Path.Data>
<PathGeometry Figures="M38.5796 38.4043L47.7906 55.2626H30.6999L22.9226 40.8556H15.5546V55.2626H0V4.19308H28.4486C40.2169 4.19308 47.6883 10.7246 47.6883 22.4706C47.6883 30.0289 44.209 35.4433 38.5796 38.4043ZM15.5546 17.4658V27.5775H26.6066C29.8813 27.5775 31.9279 25.6369 31.9279 22.4706C31.9279 19.3043 29.8813 17.4658 26.6066 17.4658H15.5546ZM97.2175 59.0374L50.656 46.4743L63.1406 0L86.4214 6.28155L109.702 12.5631L97.2175 59.0374ZM88.4169 24.8198L75.4206 21.2449L71.9413 34.2166L84.9376 37.7925L88.4169 24.8198ZM163.019 40.8556C163.019 50.661 156.777 55.2626 147.055 55.2626H116.56V4.19308H146.032C155.753 4.19308 161.995 9.19789 161.995 17.9818C161.995 23.4973 159.949 27.1754 156.06 29.7289C160.461 31.6631 163.019 35.5455 163.019 40.8556ZM131.705 16.4498V24.008H141.83C144.593 24.008 146.231 22.7824 146.231 20.1268C146.231 17.6754 144.593 16.4498 141.83 16.4498H131.705ZM131.705 43.0059H143.064C145.725 43.0059 147.265 41.576 147.265 39.1235C147.265 36.469 145.73 35.2433 143.064 35.2433H131.705V43.0059ZM170.694 4.19308H186.246V40.1417H208.555V55.2626H170.692L170.694 4.19308ZM265.762 29.7289C265.762 34.9812 264.202 40.1156 261.278 44.4827C258.355 48.8498 254.199 52.2536 249.338 54.2636C244.476 56.2736 239.126 56.7995 233.965 55.7748C228.804 54.7501 224.063 52.2209 220.342 48.5069C216.621 44.793 214.087 40.0611 213.06 34.9098C212.034 29.7584 212.561 24.4188 214.574 19.5663C216.588 14.7138 219.998 10.5663 224.374 7.64828C228.749 4.73025 233.893 3.17276 239.156 3.17276C242.651 3.16582 246.114 3.8478 249.345 5.17958C252.575 6.51135 255.511 8.46672 257.983 10.9335C260.455 13.4003 262.415 16.3299 263.75 19.5544C265.085 22.7788 265.769 26.2346 265.762 29.7235V29.7289ZM250.208 29.7289C250.208 23.3952 245.193 18.3904 239.156 18.3904C233.118 18.3904 228.103 23.3952 228.103 29.7289C228.103 36.0626 233.118 41.0663 239.156 41.0663C245.193 41.0663 250.208 36.0551 250.208 29.7235V29.7289ZM303.216 28.9107L320 55.2626H301.472L292.267 40.2428L282.75 55.2626H263.92L281.419 29.5225L265.353 4.19308H283.875L292.369 17.9818L300.556 4.19308H318.976L303.216 28.9107Z" FillRule="NonZero"/>
</Path.Data>
</Path>
<Grid Margin="0,0,0,29" VerticalAlignment="Bottom">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="0,0,0,23" TextAlignment="Center" Grid.Row="0" Text="{Binding Message}" Foreground="{Binding Foreground}" FontFamily="{StaticResource Rubik}" FontSize="17" FontWeight="Light">
<TextBlock.LayoutTransform>
<ScaleTransform ScaleY="0.9"/>
</TextBlock.LayoutTransform>
</TextBlock>
<ProgressBar Grid.Row="1" Width="480" Height="12" Foreground="{Binding Foreground}" Background="{Binding ProgressBarBackground}" BorderThickness="0" IsIndeterminate="{Binding ProgressIndeterminate}" Value="{Binding ProgressValue}"></ProgressBar>
</Grid>
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,97 @@
using System.Windows;
using System.Windows.Forms;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Bloxstrap.UI.Elements.Bootstrapper.Base;
using Bloxstrap.UI.ViewModels.Bootstrapper;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
/// <summary>
/// Interaction logic for ByfronDialog.xaml
/// </summary>
public partial class ByfronDialog : IBootstrapperDialog
{
private readonly ByfronDialogViewModel _viewModel;
public Bloxstrap.Bootstrapper? Bootstrapper { get; set; }
#region UI Elements
public string Message
{
get => _viewModel.Message;
set
{
_viewModel.Message = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.Message));
}
}
public ProgressBarStyle ProgressStyle
{
get => _viewModel.ProgressIndeterminate ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous;
set
{
_viewModel.ProgressIndeterminate = (value == ProgressBarStyle.Marquee);
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressIndeterminate));
}
}
public int ProgressValue
{
get => _viewModel.ProgressValue;
set
{
_viewModel.ProgressValue = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressValue));
}
}
public bool CancelEnabled
{
get => _viewModel.CancelEnabled;
set
{
_viewModel.CancelEnabled = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelEnabled));
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility));
_viewModel.OnPropertyChanged(nameof(_viewModel.VersionTextVisibility));
_viewModel.OnPropertyChanged(nameof(_viewModel.VersionText));
}
}
#endregion
public ByfronDialog()
{
_viewModel = new ByfronDialogViewModel(this);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
if (App.Settings.Prop.Theme.GetFinal() == Theme.Light)
{
// Matching the roblox website light theme as close as possible.
_viewModel.DialogBorder = new Thickness(1);
_viewModel.Background = new SolidColorBrush(Color.FromRgb(242, 244, 245));
_viewModel.Foreground = new SolidColorBrush(Color.FromRgb(57, 59, 61));
_viewModel.IconColor = new SolidColorBrush(Color.FromRgb(57, 59, 61));
_viewModel.ProgressBarBackground = new SolidColorBrush(Color.FromRgb(189, 190, 190));
_viewModel.ByfronLogoLocation = new BitmapImage(new Uri("pack://application:,,,/Resources/BootstrapperStyles/ByfronDialog/ByfronLogoLight.jpg"));
}
InitializeComponent();
}
#region IBootstrapperDialog Methods
// Referencing FluentDialog
public void ShowBootstrapper() => this.ShowDialog();
public void CloseBootstrapper() => Dispatcher.BeginInvoke(this.Close);
public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback);
#endregion
}
}

View File

@ -1,31 +1,34 @@
<ui:UiWindow x:Class="Bloxstrap.Dialogs.FluentDialog" <ui:UiWindow x:Class="Bloxstrap.UI.Elements.Bootstrapper.FluentDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.Dialogs"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
mc:Ignorable="d" mc:Ignorable="d"
Width="420" Width="420"
Height="192" MinHeight="0"
MinHeight="191" SizeToContent="Height"
MaxHeight="192"
ResizeMode="NoResize" ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}" Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True" ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<StackPanel> <Grid>
<ui:TitleBar Background="{ui:ThemeResource ApplicationBackgroundBrush}" Title="{Binding Title, Mode=OneTime}" Padding="15,15,0,10" x:Name="RootTitleBar" Grid.Row="0" ForceShutdown="False" MinimizeToTray="False" ShowHelp="False" UseSnapLayout="False" ShowClose="False" ShowMinimize="False" ShowMaximize="False" /> <Grid.RowDefinitions>
<Grid Margin="16,0,16,0"> <RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" x:Name="RootTitleBar" Padding="8" Title="{Binding Title, Mode=OneTime}" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" ShowClose="False" />
<Grid Grid.Row="1" Margin="16,8,16,16">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="0,12,0,0" Width="48" Height="48" VerticalAlignment="Top"> <Border Grid.Column="0" Margin="0,12,0,0" Width="48" Height="48" VerticalAlignment="Top">
<Border.Background> <Border.Background>
<ImageBrush ImageSource="{Binding Icon, Mode=OneWay}" /> <ImageBrush ImageSource="{Binding Icon, Mode=OneWay}" RenderOptions.BitmapScalingMode="HighQuality" />
</Border.Background> </Border.Background>
</Border> </Border>
<StackPanel Grid.Column="1"> <StackPanel Grid.Column="1">
@ -33,8 +36,9 @@
<ProgressBar Margin="16,16,0,16" IsIndeterminate="{Binding ProgressIndeterminate, Mode=OneWay}" Value="{Binding ProgressValue, Mode=OneWay}" /> <ProgressBar Margin="16,16,0,16" IsIndeterminate="{Binding ProgressIndeterminate, Mode=OneWay}" Value="{Binding ProgressValue, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Margin="8,16,16,16" Content="{Binding CancelButtonText, Mode=OneWay}" Width="120" HorizontalAlignment="Right" Visibility="{Binding CancelButtonVisibility, Mode=OneWay}" Command="{Binding CancelInstallCommand}" /> <Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
</StackPanel> <Button Margin="0" Content="Cancel" Width="120" HorizontalAlignment="Right" IsEnabled="{Binding CancelEnabled, Mode=OneWay}" Command="{Binding CancelInstallCommand}" />
</StackPanel> </Border>
</Grid>
</ui:UiWindow> </ui:UiWindow>

View File

@ -1,16 +1,13 @@
using System; using System.Windows.Forms;
using System.Windows;
using System.Windows.Forms;
using Bloxstrap.Enums;
using Bloxstrap.Extensions;
using Bloxstrap.ViewModels;
using Wpf.Ui.Appearance; using Wpf.Ui.Appearance;
using Wpf.Ui.Mvvm.Contracts; using Wpf.Ui.Mvvm.Contracts;
using Wpf.Ui.Mvvm.Services; using Wpf.Ui.Mvvm.Services;
namespace Bloxstrap.Dialogs using Bloxstrap.UI.ViewModels.Bootstrapper;
using Bloxstrap.UI.Elements.Bootstrapper.Base;
namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
/// <summary> /// <summary>
/// Interaction logic for FluentDialog.xaml /// Interaction logic for FluentDialog.xaml
@ -19,9 +16,9 @@ namespace Bloxstrap.Dialogs
{ {
private readonly IThemeService _themeService = new ThemeService(); private readonly IThemeService _themeService = new ThemeService();
private readonly FluentDialogViewModel _viewModel; private readonly BootstrapperDialogViewModel _viewModel;
public Bootstrapper? Bootstrapper { get; set; } public Bloxstrap.Bootstrapper? Bootstrapper { get; set; }
#region UI Elements #region UI Elements
public string Message public string Message
@ -56,18 +53,20 @@ namespace Bloxstrap.Dialogs
public bool CancelEnabled public bool CancelEnabled
{ {
get => _viewModel.CancelButtonVisibility == Visibility.Visible; get => _viewModel.CancelEnabled;
set set
{ {
_viewModel.CancelButtonVisibility = (value ? Visibility.Visible : Visibility.Collapsed); _viewModel.CancelEnabled = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility)); _viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility));
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelEnabled));
} }
} }
#endregion #endregion
public FluentDialog() public FluentDialog()
{ {
_viewModel = new FluentDialogViewModel(this); _viewModel = new BootstrapperDialogViewModel(this);
DataContext = _viewModel; DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle; Title = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource(); Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
@ -84,31 +83,7 @@ namespace Bloxstrap.Dialogs
public void CloseBootstrapper() => Dispatcher.BeginInvoke(this.Close); public void CloseBootstrapper() => Dispatcher.BeginInvoke(this.Close);
// TODO: make prompts use dialog view natively rather than using message dialog boxes public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback);
public void ShowSuccess(string message)
{
App.ShowMessageBox(message, MessageBoxImage.Information);
App.Terminate();
}
public void ShowError(string message)
{
App.ShowMessageBox($"An error occurred while starting Roblox\n\nDetails: {message}", MessageBoxImage.Error);
App.Terminate(Bootstrapper.ERROR_INSTALL_FAILURE);
}
public void PromptShutdown()
{
MessageBoxResult result = App.ShowMessageBox(
"Roblox is currently running, but needs to close. Would you like close Roblox now?",
MessageBoxImage.Information,
MessageBoxButton.OKCancel
);
if (result != MessageBoxResult.OK)
Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT);
}
#endregion #endregion
} }
} }

View File

@ -1,8 +1,8 @@
using System.Windows.Forms; using System.Windows.Forms;
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
partial class LegacyDialog2009 partial class LegacyDialog2008
{ {
/// <summary> /// <summary>
/// Required designer variable. /// Required designer variable.
@ -65,7 +65,7 @@ namespace Bloxstrap.Dialogs
this.buttonCancel.UseVisualStyleBackColor = true; this.buttonCancel.UseVisualStyleBackColor = true;
this.buttonCancel.Click += new System.EventHandler(this.ButtonCancel_Click); this.buttonCancel.Click += new System.EventHandler(this.ButtonCancel_Click);
// //
// LegacyDialog2009 // LegacyDialog2008
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
@ -79,10 +79,10 @@ namespace Bloxstrap.Dialogs
this.MaximumSize = new System.Drawing.Size(327, 161); this.MaximumSize = new System.Drawing.Size(327, 161);
this.MinimizeBox = false; this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(327, 161); this.MinimumSize = new System.Drawing.Size(327, 161);
this.Name = "LegacyDialog2009"; this.Name = "LegacyDialog2008";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "LegacyDialog2009"; this.Text = "LegacyDialog2008";
this.Load += new System.EventHandler(this.LegacyDialog2009_Load); this.Load += new System.EventHandler(this.LegacyDialog2008_Load);
this.ResumeLayout(false); this.ResumeLayout(false);
} }

View File

@ -1,12 +1,13 @@
using System;
using System.Windows.Forms; using System.Windows.Forms;
namespace Bloxstrap.Dialogs using Bloxstrap.UI.Elements.Bootstrapper.Base;
namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
// windows: https://youtu.be/VpduiruysuM?t=18 // windows: https://youtu.be/VpduiruysuM?t=18
// mac: https://youtu.be/ncHhbcVDRgQ?t=63 // mac: https://youtu.be/ncHhbcVDRgQ?t=63
public partial class LegacyDialog2009 : BootstrapperDialogForm public partial class LegacyDialog2008 : WinFormsDialogBase
{ {
protected override string _message protected override string _message
{ {
@ -32,7 +33,7 @@ namespace Bloxstrap.Dialogs
set => this.buttonCancel.Enabled = value; set => this.buttonCancel.Enabled = value;
} }
public LegacyDialog2009() public LegacyDialog2008()
{ {
InitializeComponent(); InitializeComponent();
@ -40,7 +41,7 @@ namespace Bloxstrap.Dialogs
SetupDialog(); SetupDialog();
} }
private void LegacyDialog2009_Load(object sender, EventArgs e) private void LegacyDialog2008_Load(object sender, EventArgs e)
{ {
this.Activate(); this.Activate();
} }

View File

@ -1,6 +1,6 @@
using System.Windows.Forms; using System.Windows.Forms;
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
partial class LegacyDialog2011 partial class LegacyDialog2011
{ {

View File

@ -1,13 +1,12 @@
using System;
using System.Windows.Forms; using System.Windows.Forms;
using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper.Base;
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
// https://youtu.be/3K9oCEMHj2s?t=35 // https://youtu.be/3K9oCEMHj2s?t=35
public partial class LegacyDialog2011 : BootstrapperDialogForm public partial class LegacyDialog2011 : WinFormsDialogBase
{ {
protected override string _message protected override string _message
{ {

View File

@ -1,6 +1,6 @@
using System.Windows.Forms; using System.Windows.Forms;
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
partial class ProgressDialog partial class ProgressDialog
{ {

View File

@ -1,15 +1,13 @@
using System; using System.Drawing;
using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
using Bloxstrap.Enums; using Bloxstrap.UI.Elements.Bootstrapper.Base;
using Bloxstrap.Extensions;
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
// basically just the modern dialog // basically just the modern dialog
public partial class ProgressDialog : BootstrapperDialogForm public partial class ProgressDialog : WinFormsDialogBase
{ {
protected override string _message protected override string _message
{ {

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
partial class VistaDialog partial class VistaDialog
{ {

View File

@ -1,9 +1,8 @@
using System; using System.Windows.Forms;
using System.Windows.Forms;
using Bloxstrap.Extensions; using Bloxstrap.UI.Elements.Bootstrapper.Base;
namespace Bloxstrap.Dialogs namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
// https://youtu.be/h0_AL95Sc3o?t=48 // https://youtu.be/h0_AL95Sc3o?t=48
@ -11,7 +10,7 @@ namespace Bloxstrap.Dialogs
// since taskdialog is part of winforms, it can't really be properly used without a form // since taskdialog is part of winforms, it can't really be properly used without a form
// for example, cross-threaded calls to ui controls can't really be done outside of a form // for example, cross-threaded calls to ui controls can't really be done outside of a form
public partial class VistaDialog : BootstrapperDialogForm public partial class VistaDialog : WinFormsDialogBase
{ {
private TaskDialogPage _dialogPage; private TaskDialogPage _dialogPage;
@ -80,11 +79,11 @@ namespace Bloxstrap.Dialogs
SetupDialog(); SetupDialog();
} }
public override void ShowSuccess(string message) public override void ShowSuccess(string message, Action? callback)
{ {
if (this.InvokeRequired) if (this.InvokeRequired)
{ {
this.Invoke(ShowSuccess, message); this.Invoke(ShowSuccess, message, callback);
} }
else else
{ {
@ -96,43 +95,19 @@ namespace Bloxstrap.Dialogs
Buttons = { TaskDialogButton.OK } Buttons = { TaskDialogButton.OK }
}; };
successDialog.Buttons[0].Click += (_, _) => App.Terminate(); successDialog.Buttons[0].Click += (_, _) =>
{
if (callback is not null)
callback();
App.Terminate();
};
_dialogPage.Navigate(successDialog); _dialogPage.Navigate(successDialog);
_dialogPage = successDialog; _dialogPage = successDialog;
} }
} }
public override void ShowError(string message)
{
if (this.InvokeRequired)
{
this.Invoke(ShowError, message);
}
else
{
TaskDialogPage errorDialog = new()
{
Icon = TaskDialogIcon.Error,
Caption = App.Settings.Prop.BootstrapperTitle,
Heading = "An error occurred while starting Roblox",
Buttons = { TaskDialogButton.Close },
Expander = new TaskDialogExpander()
{
Text = message,
CollapsedButtonText = "See details",
ExpandedButtonText = "Hide details",
Position = TaskDialogExpanderPosition.AfterText
}
};
errorDialog.Buttons[0].Click += (sender, e) => App.Terminate(Bootstrapper.ERROR_INSTALL_FAILURE);
_dialogPage.Navigate(errorDialog);
_dialogPage = errorDialog;
}
}
public override void CloseBootstrapper() public override void CloseBootstrapper()
{ {
if (this.InvokeRequired) if (this.InvokeRequired)

View File

@ -0,0 +1,62 @@
<ui:UiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.LogTracer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
d:DataContext="{d:DesignInstance Type=models:LogTracerViewModel}"
mc:Ignorable="d"
Title="Log tracer"
Width="800"
Height="480"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" Title="Log tracer" ShowMinimize="True" ShowMaximize="True" CanMaximize="True" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<StackPanel Grid.Row="1" Orientation="Horizontal" Background="{ui:ThemeResource ControlFillColorDefaultBrush}">
<ui:MenuItem Margin="4,8,0,8" Header="Keep on top" IsCheckable="True" Click="KeepOnTopMenuItem_Click" />
<ui:MenuItem Margin="0,8,0,8" Header="Scroll to end" IsCheckable="True" IsChecked="True" Click="AutoScrollMenuItem_Click" />
<ui:MenuItem Name="TextWrappingToggle" Margin="0,8,0,8" Header="Text wrapping" IsCheckable="True" IsChecked="False" />
<ui:MenuItem Margin="0,8,4,8" Header="Locate log file" Command="{Binding LocateLogFileCommand, Mode=OneTime}" />
</StackPanel>
<ScrollViewer x:Name="ScrollViewer" Grid.Row="2" VerticalAlignment="Top">
<ScrollViewer.Style>
<Style TargetType="ScrollViewer">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=TextWrappingToggle, Path=IsChecked}" Value="True">
<Setter Property="HorizontalScrollBarVisibility" Value="Disabled" />
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=TextWrappingToggle, Path=IsChecked}" Value="False">
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
</DataTrigger>
</Style.Triggers>
</Style>
</ScrollViewer.Style>
<TextBox Padding="0" Background="Transparent" TextWrapping="WrapWithOverflow" BorderThickness="0" IsReadOnly="True" FontFamily="Courier New" Text="{Binding LogContents, Mode=OneWay}" TextChanged="TextBox_TextChanged" />
</ScrollViewer>
<StatusBar Grid.Row="3" Padding="8">
<StatusBarItem>
<TextBlock>
<TextBlock.Text>
<MultiBinding Mode="OneWay" StringFormat="Tracing {0}">
<Binding Path="LogFilename" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StatusBarItem>
</StatusBar>
</Grid>
</ui:UiWindow>

View File

@ -0,0 +1,31 @@
using System.Windows;
using System.Windows.Controls;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
{
/// <summary>
/// Interaction logic for LogTracer.xaml
/// </summary>
public partial class LogTracer
{
private bool _autoscroll = true;
public LogTracer(RobloxActivity activityWatcher)
{
DataContext = new LogTracerViewModel(this, activityWatcher);
InitializeComponent();
}
private void KeepOnTopMenuItem_Click(object sender, RoutedEventArgs e) => Topmost = ((MenuItem)sender).IsChecked;
private void AutoScrollMenuItem_Click(object sender, RoutedEventArgs e) => _autoscroll = ((MenuItem)sender).IsChecked;
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_autoscroll)
ScrollViewer.ScrollToEnd();
}
}
}

View File

@ -0,0 +1,32 @@
<ui:UiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.MenuContainer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
mc:Ignorable="d"
Title="ContextMenuContainer"
MinWidth="0"
MinHeight="0"
Width="0"
Height="0"
Top="-100"
Left="-100"
ShowInTaskbar="False"
WindowStyle="None"
ResizeMode="NoResize"
WindowStartupLocation="Manual"
Loaded="Window_Loaded"
Closed="Window_Closed">
<ui:UiWindow.ContextMenu>
<ContextMenu>
<MenuItem x:Name="VersionMenuItem" IsEnabled="False" />
<Separator />
<MenuItem x:Name="RichPresenceMenuItem" Header="Discord Rich Presence" IsCheckable="True" IsChecked="True" Visibility="Collapsed" Click="RichPresenceMenuItem_Click" />
<MenuItem x:Name="InviteDeeplinkMenuItem" Header="Copy invite deeplink" Visibility="Collapsed" Click="InviteDeeplinkMenuItem_Click" />
<MenuItem x:Name="ServerDetailsMenuItem" Header="See server details" Visibility="Collapsed" Click="ServerDetailsMenuItem_Click" />
<MenuItem x:Name="LogTracerMenuItem" Header="Open log tracer" Visibility="Collapsed" Click="LogTracerMenuItem_Click" />
</ContextMenu>
</ui:UiWindow.ContextMenu>
</ui:UiWindow>

View File

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Bloxstrap.Integrations;
namespace Bloxstrap.UI.Elements.ContextMenu
{
/// <summary>
/// Interaction logic for NotifyIconMenu.xaml
/// </summary>
public partial class MenuContainer
{
// i wouldve gladly done this as mvvm but turns out that data binding just does not work with menuitems for some reason so idk this sucks
private readonly RobloxActivity? _activityWatcher;
private readonly DiscordRichPresence? _richPresenceHandler;
private LogTracer? _logTracerWindow;
private ServerInformation? _serverInformationWindow;
public MenuContainer(RobloxActivity? activityWatcher, DiscordRichPresence? richPresenceHandler)
{
InitializeComponent();
_activityWatcher = activityWatcher;
_richPresenceHandler = richPresenceHandler;
if (_activityWatcher is not null)
{
if (App.Settings.Prop.OhHeyYouFoundMe)
LogTracerMenuItem.Visibility = Visibility.Visible;
_activityWatcher.OnGameJoin += ActivityWatcher_OnGameJoin;
_activityWatcher.OnGameLeave += ActivityWatcher_OnGameLeave;
}
if (_richPresenceHandler is not null)
RichPresenceMenuItem.Visibility = Visibility.Visible;
VersionMenuItem.Header = $"{App.ProjectName} v{App.Version}";
}
public void ShowServerInformationWindow()
{
if (_serverInformationWindow is null)
{
_serverInformationWindow = new ServerInformation(_activityWatcher!);
_serverInformationWindow.Closed += (_, _) => _serverInformationWindow = null;
}
if (!_serverInformationWindow.IsVisible)
_serverInformationWindow.Show();
_serverInformationWindow.Activate();
}
private void ActivityWatcher_OnGameJoin(object? sender, EventArgs e)
{
Dispatcher.Invoke(() => {
if (_activityWatcher?.ActivityServerType == ServerType.Public)
InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
ServerDetailsMenuItem.Visibility = Visibility.Visible;
});
}
private void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
{
Dispatcher.Invoke(() => {
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
ServerDetailsMenuItem.Visibility = Visibility.Collapsed;
_serverInformationWindow?.Close();
});
}
private void Window_Loaded(object? sender, RoutedEventArgs e)
{
// this is an awful hack lmao im so sorry to anyone who reads this
// this is done to register the context menu wrapper as a tool window so it doesnt appear in the alt+tab switcher
// https://stackoverflow.com/a/551847/11852173
var wndHelper = new WindowInteropHelper(this);
long exStyle = NativeMethods.GetWindowLongPtr(wndHelper.Handle, NativeMethods.GWL_EXSTYLE).ToInt64();
exStyle |= NativeMethods.WS_EX_TOOLWINDOW;
NativeMethods.SetWindowLongPtr(wndHelper.Handle, NativeMethods.GWL_EXSTYLE, (IntPtr)exStyle);
}
private void Window_Closed(object sender, EventArgs e) => App.Logger.WriteLine("[MenuContainer::Window_Closed] Context menu container closed");
private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _richPresenceHandler?.SetVisibility(((MenuItem)sender).IsChecked);
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetText($"roblox://experiences/start?placeId={_activityWatcher?.ActivityPlaceId}&gameInstanceId={_activityWatcher?.ActivityJobId}");
private void ServerDetailsMenuItem_Click(object sender, RoutedEventArgs e) => ShowServerInformationWindow();
private void LogTracerMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_logTracerWindow is null)
{
_logTracerWindow = new LogTracer(_activityWatcher!);
_logTracerWindow.Closed += (_, _) => _logTracerWindow = null;;
}
if (!_logTracerWindow.IsVisible)
_logTracerWindow.Show();
_logTracerWindow.Activate();
}
}
}

View File

@ -0,0 +1,61 @@
<ui:UiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.ServerInformation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
d:DataContext="{d:DesignInstance Type=models:ServerInformationViewModel}"
mc:Ignorable="d"
Title="Server information"
MinWidth="0"
MinHeight="0"
Width="420"
SizeToContent="Height"
ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" Title="Server information" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<Grid Grid.Row="1" Margin="16,8,16,16">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="Type" />
<TextBlock Grid.Row="0" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ServerType, Mode=OneWay}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="Instance ID" />
<TextBlock Grid.Row="1" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding InstanceId, Mode=OneWay}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="Location" />
<TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ServerLocation, Mode=OneWay}" />
<TextBlock Grid.Row="3" Grid.Column="0" Margin="0,0,16,0" VerticalAlignment="Center" Text="UDMUX proxied" />
<TextBlock Grid.Row="3" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding UdmuxProxied, Mode=OneWay}" />
</Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button MinWidth="100" Content="Copy Instance ID" Command="{Binding CopyInstanceIdCommand, Mode=OneTime}" />
<Button Margin="12,0,0,0" MinWidth="100" Content="Close" Command="{Binding CloseWindowCommand, Mode=OneTime}" />
</StackPanel>
</Border>
</Grid>
</ui:UiWindow>

View File

@ -1,5 +1,4 @@
using Bloxstrap.ViewModels; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -11,19 +10,20 @@ using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
namespace Bloxstrap.Views.Pages using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
{ {
/// <summary> /// <summary>
/// Interaction logic for AppearancePage.xaml /// Interaction logic for ServerInformation.xaml
/// </summary> /// </summary>
public partial class AppearancePage public partial class ServerInformation
{ {
public AppearancePage() public ServerInformation(RobloxActivity activityWatcher)
{ {
DataContext = new AppearanceViewModel(this); DataContext = new ServerInformationViewModel(this, activityWatcher);
InitializeComponent(); InitializeComponent();
} }
} }

View File

@ -0,0 +1,60 @@
<ui:UiWindow x:Class="Bloxstrap.UI.Elements.Dialogs.AddFastFlagDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
mc:Ignorable="d"
Title="Add FastFlag"
MinHeight="0"
Width="480"
SizeToContent="Height"
ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" Title="Add FastFlag" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" />
<Grid Grid.Row="1" Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" MinWidth="100" Text="Flag name" Margin="0,0,0,12" />
<TextBox Grid.Row="0" Grid.Column="1" Name="FlagNameTextBox" Margin="0,0,0,12" />
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" MinWidth="100" Text="Flag value" />
<TextBox Grid.Row="1" Grid.Column="1" Name="FlagValueTextBox" />
</Grid>
<Border Grid.Row="2" Margin="0,10,0,0" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button MinWidth="100" Content="OK" Click="OKButton_Click">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=FlagNameTextBox, Path=Text.Length}" Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button MinWidth="100" Margin="12,0,0,0" Content="Cancel" IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</ui:UiWindow>

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Bloxstrap.UI.Elements.Dialogs
{
/// <summary>
/// Interaction logic for AddFlagDialog.xaml
/// </summary>
public partial class AddFastFlagDialog
{
public MessageBoxResult Result = MessageBoxResult.Cancel;
public AddFastFlagDialog()
{
InitializeComponent();
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
Result = MessageBoxResult.OK;
Close();
}
}
}

View File

@ -0,0 +1,49 @@
<ui:UiWindow x:Class="Bloxstrap.UI.Elements.Dialogs.ExceptionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
mc:Ignorable="d"
Width="480"
MinHeight="0"
SizeToContent="Height"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" />
<Grid Grid.Row="1" Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Top" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Error.png" />
<StackPanel Grid.Column="1">
<TextBlock Text="An exception occurred while running Bloxstrap" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,16,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" />
<TextBlock Text="Please report this exception through a GitHub issue or in our Discord chat, along with a copy of the log file that was created." Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
</Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button x:Name="LocateLogFileButton" Content="Locate log file" />
<ComboBox x:Name="ReportOptions" SelectedIndex="0" Padding="12,6,12,6" Margin="12,0,0,0">
<ComboBoxItem Content="Submit report..." Visibility="Collapsed" />
<ComboBoxItem Content="Submit report via GitHub" />
<ComboBoxItem Content="Submit report via Discord" />
</ComboBox>
<Button x:Name="CloseButton" MinWidth="100" Content="Close" Margin="12,0,0,0" />
</StackPanel>
</Border>
</Grid>
</ui:UiWindow>

Some files were not shown because too many files have changed in this diff Show More