Merge pull request #488 from pizzaboxer/version-2.4.1

Version 2.5.0
This commit is contained in:
pizzaboxer 2023-08-01 21:22:35 +01:00 committed by GitHub
commit dca6fc46bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1734 additions and 820 deletions

View File

@ -2,7 +2,8 @@
using System.Windows;
using System.Windows.Threading;
using Microsoft.Win32;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Bloxstrap
{
@ -17,10 +18,13 @@ namespace Bloxstrap
// used only for communicating between app and menu - use Directories.Base for anything else
public static string BaseDirectory = null!;
public static string? CustomFontLocation;
public static bool ShouldSaveConfigs { get; set; } = false;
public static bool IsSetupComplete { get; set; } = true;
public static bool IsFirstRun { get; private set; } = true;
public static bool IsFirstRun { get; set; } = true;
public static bool IsQuiet { get; private set; } = false;
public static bool IsUninstall { get; private set; } = false;
public static bool IsNoLaunch { get; private set; } = false;
@ -39,7 +43,11 @@ namespace Bloxstrap
public static readonly JsonManager<State> State = new();
public static readonly FastFlagManager FastFlags = new();
public static readonly HttpClient HttpClient = new(new HttpClientLoggingHandler(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }));
public static readonly HttpClient HttpClient = new(
new HttpClientLoggingHandler(
new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }
)
);
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
@ -51,7 +59,7 @@ namespace Bloxstrap
int exitCodeNum = (int)exitCode;
Logger.WriteLine($"[App::Terminate] Terminating with exit code {exitCodeNum} ({exitCode})");
Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
Settings.Save();
State.Save();
@ -64,14 +72,16 @@ namespace Bloxstrap
{
e.Handled = true;
Logger.WriteLine("[App::OnStartup] An exception occurred when running the main thread");
Logger.WriteLine($"[App::OnStartup] {e.Exception}");
Logger.WriteLine("App::GlobalExceptionHandler", "An exception occurred");
FinalizeExceptionHandling(e.Exception);
}
void FinalizeExceptionHandling(Exception exception)
public static void FinalizeExceptionHandling(Exception exception, bool log = true)
{
if (log)
Logger.WriteException("App::FinalizeExceptionHandling", exception);
#if DEBUG
throw exception;
#else
@ -84,14 +94,18 @@ namespace Bloxstrap
protected override void OnStartup(StartupEventArgs e)
{
const string LOG_IDENT = "App::OnStartup";
base.OnStartup(e);
Logger.WriteLine($"[App::OnStartup] Starting {ProjectName} v{Version}");
Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}");
if (String.IsNullOrEmpty(BuildMetadata.CommitHash))
Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
else
Logger.WriteLine($"[App::OnStartup] Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})");
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})");
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
@ -99,79 +113,75 @@ namespace Bloxstrap
LaunchArgs = e.Args;
HttpClient.Timeout = TimeSpan.FromMinutes(5);
HttpClient.Timeout = TimeSpan.FromSeconds(30);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
if (LaunchArgs.Length > 0)
{
if (Array.IndexOf(LaunchArgs, "-preferences") != -1 || Array.IndexOf(LaunchArgs, "-menu") != -1)
{
Logger.WriteLine("[App::OnStartup] Started with IsMenuLaunch flag");
Logger.WriteLine(LOG_IDENT, "Started with IsMenuLaunch flag");
IsMenuLaunch = true;
}
if (Array.IndexOf(LaunchArgs, "-quiet") != -1)
{
Logger.WriteLine("[App::OnStartup] Started with IsQuiet flag");
Logger.WriteLine(LOG_IDENT, "Started with IsQuiet flag");
IsQuiet = true;
}
if (Array.IndexOf(LaunchArgs, "-uninstall") != -1)
{
Logger.WriteLine("[App::OnStartup] Started with IsUninstall flag");
Logger.WriteLine(LOG_IDENT, "Started with IsUninstall flag");
IsUninstall = true;
}
if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1)
{
Logger.WriteLine("[App::OnStartup] Started with IsNoLaunch flag");
Logger.WriteLine(LOG_IDENT, "Started with IsNoLaunch flag");
IsNoLaunch = true;
}
if (Array.IndexOf(LaunchArgs, "-upgrade") != -1)
{
Logger.WriteLine("[App::OnStartup] Bloxstrap started with IsUpgrade flag");
Logger.WriteLine(LOG_IDENT, "Bloxstrap started with IsUpgrade flag");
IsUpgrade = true;
}
}
// check if installed
using (RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"))
if (!IsMenuLaunch)
{
string? installLocation = null;
Logger.WriteLine(LOG_IDENT, "Performing connectivity check...");
if (registryKey is not null)
installLocation = (string?)registryKey.GetValue("InstallLocation");
if (registryKey is null || installLocation is null)
try
{
Logger.WriteLine("[App::OnStartup] Running first-time install");
BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName);
Logger.Initialize(true);
if (!IsQuiet)
{
IsSetupComplete = false;
FastFlags.Load();
Controls.ShowMenu();
}
HttpClient.GetAsync("https://detectportal.firefox.com").Wait();
Logger.WriteLine(LOG_IDENT, "Connectivity check finished");
}
else
catch (Exception ex)
{
IsFirstRun = false;
BaseDirectory = installLocation;
Logger.WriteLine(LOG_IDENT, "Connectivity check failed!");
Logger.WriteException(LOG_IDENT, ex);
if (ex.GetType() == typeof(AggregateException))
ex = ex.InnerException!;
Controls.ShowConnectivityDialog(
"the internet",
$"Something may be preventing {ProjectName} from connecting to the internet, or you are currently offline. Please check and try again.",
ex
);
Terminate(ErrorCode.ERROR_CANCELLED);
}
}
// exit if we don't click the install button on installation
if (!IsSetupComplete)
using (var checker = new InstallChecker())
{
Logger.WriteLine("[App::OnStartup] Installation cancelled!");
Terminate(ErrorCode.ERROR_CANCELLED);
checker.Check();
}
Directories.Initialize(BaseDirectory);
Paths.Initialize(BaseDirectory);
// we shouldn't save settings on the first run until the first installation is finished,
// just in case the user decides to cancel the install
@ -181,7 +191,7 @@ namespace Bloxstrap
if (!Logger.Initialized)
{
Logger.WriteLine("[App::OnStartup] Possible duplicate launch detected, terminating.");
Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating.");
Terminate();
}
@ -195,7 +205,7 @@ namespace Bloxstrap
#if !DEBUG
if (!IsUninstall && !IsFirstRun)
Updater.CheckInstalledVersion();
InstallChecker.CheckUpgrade();
#endif
string commandLine = "";
@ -206,9 +216,9 @@ namespace Bloxstrap
if (menuProcess is not null)
{
IntPtr handle = menuProcess.MainWindowHandle;
Logger.WriteLine($"[App::OnStartup] Found an already existing menu window with handle {handle}");
NativeMethods.SetForegroundWindow(handle);
var handle = menuProcess.MainWindowHandle;
Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}");
PInvoke.SetForegroundWindow((HWND)handle);
}
else
{
@ -253,13 +263,13 @@ namespace Bloxstrap
ShouldSaveConfigs = true;
// start bootstrapper and show the bootstrapper modal if we're not running silently
Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper");
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
Bootstrapper bootstrapper = new(commandLine);
IBootstrapperDialog? dialog = null;
if (!IsQuiet)
{
Logger.WriteLine($"[App::OnStartup] Initializing bootstrapper dialog");
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog");
dialog = Settings.Prop.BootstrapperStyle.GetNew();
bootstrapper.Dialog = dialog;
dialog.Bootstrapper = bootstrapper;
@ -273,12 +283,12 @@ namespace Bloxstrap
if (Settings.Prop.MultiInstanceLaunching)
{
Logger.WriteLine("[App::OnStartup] Creating singleton mutex");
Logger.WriteLine(LOG_IDENT, "Creating singleton mutex");
try
{
Mutex.OpenExisting("ROBLOX_singletonMutex");
Logger.WriteLine("[App::OnStartup] Warning - singleton mutex already exists!");
Logger.WriteLine(LOG_IDENT, "Warning - singleton mutex already exists!");
}
catch
{
@ -287,22 +297,20 @@ namespace Bloxstrap
}
}
Task bootstrapperTask = Task.Run(() => bootstrapper.Run());
bootstrapperTask.ContinueWith(t =>
Task bootstrapperTask = Task.Run(() => bootstrapper.Run()).ContinueWith(t =>
{
Logger.WriteLine("[App::OnStartup] Bootstrapper task has finished");
Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
// notifyicon is blocking main thread, must be disposed here
NotifyIcon?.Dispose();
if (t.IsFaulted)
Logger.WriteLine("[App::OnStartup] An exception occurred when running the bootstrapper");
Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is null)
return;
Logger.WriteLine($"[App::OnStartup] {t.Exception}");
Logger.WriteException(LOG_IDENT, t.Exception);
Exception exception = t.Exception;
@ -311,7 +319,7 @@ namespace Bloxstrap
exception = t.Exception.InnerException!;
#endif
FinalizeExceptionHandling(exception);
FinalizeExceptionHandling(exception, false);
});
// 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
@ -320,13 +328,13 @@ namespace Bloxstrap
if (!IsNoLaunch && Settings.Prop.EnableActivityTracking)
NotifyIcon?.InitializeContextMenu();
Logger.WriteLine($"[App::OnStartup] Waiting for bootstrapper task to finish");
Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");
bootstrapperTask.Wait();
if (singletonMutex is not null)
{
Logger.WriteLine($"[App::OnStartup] We have singleton mutex ownership! Running in background until all Roblox processes are closed");
Logger.WriteLine(LOG_IDENT, "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
@ -335,7 +343,7 @@ namespace Bloxstrap
}
}
Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating...");
Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating...");
Terminate();
}

View File

@ -7,8 +7,8 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>2.4.0</Version>
<FileVersion>2.4.0.0</FileVersion>
<Version>2.5.0</Version>
<FileVersion>2.5.0.0</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
@ -39,7 +39,11 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.18-beta">
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
</ItemGroup>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class HttpResponseException : Exception
{
public HttpResponseMessage ResponseMessage { get; }
public HttpResponseException(HttpResponseMessage responseMessage)
: base($"Could not connect to {responseMessage.RequestMessage!.RequestUri} because it returned HTTP {(int)responseMessage.StatusCode} ({responseMessage.ReasonPhrase})")
{
ResponseMessage = responseMessage;
}
}
}

View File

@ -1,18 +0,0 @@
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

@ -10,6 +10,8 @@ namespace Bloxstrap.Extensions
public static Icon GetIcon(this BootstrapperIcon icon)
{
const string LOG_IDENT = "BootstrapperIconEx::GetIcon";
// load the custom icon file
if (icon == BootstrapperIcon.IconCustom)
{
@ -21,7 +23,8 @@ namespace Bloxstrap.Extensions
}
catch (Exception ex)
{
App.Logger.WriteLine($"[BootstrapperIconEx::GetIcon] Failed to load custom icon! {ex}");
App.Logger.WriteLine(LOG_IDENT, $"Failed to load custom icon!");
App.Logger.WriteException(LOG_IDENT, ex);
}
return customIcon ?? Properties.Resources.IconBloxstrap;

View File

@ -34,7 +34,7 @@
if (emojiType == EmojiType.Default)
return "";
return $"https://github.com/NikSavchenk0/rbxcustom-fontemojis/raw/8a552f4aaaecfa58d6bd9b0540e1ac16e81faadb/{Filenames[emojiType]}";
return $"https://github.com/pizzaboxer/rbxcustom-fontemojis/releases/download/my-phone-is-78-percent/{Filenames[emojiType]}";
}
}
}

View File

@ -1,11 +1,13 @@
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Forms;
using Windows.Win32;
using Windows.Win32.Graphics.Gdi;
namespace Bloxstrap
{
public class FastFlagManager : JsonManager<Dictionary<string, object>>
{
public override string FileLocation => Path.Combine(Directories.Modifications, "ClientSettings\\ClientAppSettings.json");
public override string FileLocation => Path.Combine(Paths.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]}}";
@ -20,11 +22,10 @@ namespace Bloxstrap
{ "HTTP.Proxy.Address.3", "DFStringHttpCurlProxyHostAndPortForExternalUrl" },
{ "Rendering.Framerate", "DFIntTaskSchedulerTargetFps" },
{ "Rendering.Fullscreen", "FFlagHandleAltEnterFullscreenManually" },
{ "Rendering.ManualFullscreen", "FFlagHandleAltEnterFullscreenManually" },
{ "Rendering.TexturePack", "FStringPartTexturePackTable2022" },
{ "Rendering.DPI.Disable", "DFFlagDisableDPIScale" },
{ "Rendering.DPI.Variable", "DFFlagVariableDPIScale2" },
{ "Rendering.DisableScaling", "DFFlagDisableDPIScale" },
{ "Rendering.MSAA", "FIntDebugForceMSAASamples" },
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
@ -63,6 +64,15 @@ namespace Bloxstrap
{ "Future (Phase 3)", "Future" }
};
public static IReadOnlyDictionary<string, string?> MSAAModes => new Dictionary<string, string?>
{
{ "Automatic", null },
{ "1x MSAA", "1" },
{ "2x MSAA", "2" },
{ "4x MSAA", "4" },
{ "8x MSAA", "8" }
};
// 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?>>
@ -108,19 +118,21 @@ namespace Bloxstrap
// to delete a flag, set the value as null
public void SetValue(string key, object? value)
{
const string LOG_IDENT = "FastFlagManager::SetValue";
if (value is null)
{
if (Prop.ContainsKey(key))
App.Logger.WriteLine($"[FastFlagManager::SetValue] Deletion of '{key}' is pending");
App.Logger.WriteLine(LOG_IDENT, $"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");
App.Logger.WriteLine(LOG_IDENT, $"Changing of '{key}' from '{Prop[key]}' to '{value}' is pending");
else
App.Logger.WriteLine($"[FastFlagManager::SetValue] Setting of '{key}' to '{value}' is pending");
App.Logger.WriteLine(LOG_IDENT, $"Setting of '{key}' to '{value}' is pending");
Prop[key] = value.ToString()!;
}
@ -142,12 +154,6 @@ namespace Bloxstrap
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)))
@ -175,6 +181,14 @@ namespace Bloxstrap
return mapping.First().Key;
}
public void CheckManualFullscreenPreset()
{
if (GetPreset("Rendering.Mode.Vulkan") == "True" || GetPreset("Rendering.Mode.OpenGL") == "True")
SetPreset("Rendering.ManualFullscreen", null);
else
SetPreset("Rendering.ManualFullscreen", "False");
}
public override void Save()
{
// convert all flag values to strings before saving
@ -189,11 +203,24 @@ namespace Bloxstrap
{
base.Load();
SetPresetOnce("Rendering.Framerate", 9999);
SetPresetOnce("Rendering.Fullscreen", "False");
CheckManualFullscreenPreset();
SetPresetOnce("Rendering.DPI.Disable", "True");
SetPresetOnce("Rendering.DPI.Variable", "False");
if (GetPreset("Rendering.Framerate") is not null)
return;
// set it to be the framerate of the primary display by default
var screen = Screen.AllScreens.Where(x => x.Primary).Single();
var devmode = new DEVMODEW();
PInvoke.EnumDisplaySettings(screen.DeviceName, ENUM_DISPLAY_SETTINGS_MODE.ENUM_CURRENT_SETTINGS, ref devmode);
uint framerate = devmode.dmDisplayFrequency;
if (framerate <= 100)
framerate *= 2;
SetPreset("Rendering.Framerate", framerate);
}
}
}

View File

@ -15,8 +15,10 @@ global using System.Threading;
global using System.Threading.Tasks;
global using Bloxstrap.Enums;
global using Bloxstrap.Exceptions;
global using Bloxstrap.Extensions;
global using Bloxstrap.Models;
global using Bloxstrap.Models.BloxstrapRPC;
global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.RobloxApi;
global using Bloxstrap.UI;

View File

@ -9,13 +9,13 @@
protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
App.Logger.WriteLine($"[HttpClientLoggingHandler::HttpRequestMessage] {request.Method} {request.RequestUri}");
App.Logger.WriteLine("HttpClientLoggingHandler::ProcessRequest", $"{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}");
App.Logger.WriteLine("HttpClientLoggingHandler::ProcessResponse", $"{(int)response.StatusCode} {response.ReasonPhrase} {response.RequestMessage!.RequestUri}");
return response;
}
}

232
Bloxstrap/InstallChecker.cs Normal file
View File

@ -0,0 +1,232 @@
using System.Windows;
using Microsoft.Win32;
namespace Bloxstrap
{
internal class InstallChecker : IDisposable
{
private RegistryKey? _registryKey;
private string? _installLocation;
internal InstallChecker()
{
_registryKey = Registry.CurrentUser.OpenSubKey($"Software\\{App.ProjectName}", true);
if (_registryKey is not null)
_installLocation = (string?)_registryKey.GetValue("InstallLocation");
}
internal void Check()
{
const string LOG_IDENT = "InstallChecker::Check";
if (_registryKey is null || _installLocation is null)
{
if (!File.Exists("Settings.json") || !File.Exists("State.json"))
{
FirstTimeRun();
return;
}
_installLocation = Path.GetDirectoryName(Paths.Process)!;
App.Logger.WriteLine(LOG_IDENT, $"Registry key is likely malformed. Setting install location as '{_installLocation}'");
if (_registryKey is null)
_registryKey = Registry.CurrentUser.CreateSubKey($"Software\\{App.ProjectName}");
_registryKey.SetValue("InstallLocation", _installLocation);
}
// check if drive that bloxstrap was installed to was removed from system, or had its drive letter changed
if (!Directory.Exists(_installLocation))
{
App.Logger.WriteLine(LOG_IDENT, "Could not find install location. Checking if drive has changed...");
bool driveExists = false;
string driveName = _installLocation[..3];
string? newDriveName = null;
foreach (var drive in DriveInfo.GetDrives())
{
if (drive.Name == driveName)
driveExists = true;
else if (Directory.Exists(_installLocation.Replace(driveName, drive.Name)))
newDriveName = drive.Name;
}
if (newDriveName is not null)
{
App.Logger.WriteLine(LOG_IDENT, $"Drive has changed from {driveName} to {newDriveName}");
Controls.ShowMessageBox(
$"{App.ProjectName} has detected a drive letter change and has reconfigured its install location from the {driveName} drive to the {newDriveName} drive.\n" +
"\n" +
$"While {App.ProjectName} will continue to work, it's recommended that you change the drive leter back to its original value as other installed applications can experience similar issues.",
MessageBoxImage.Warning,
MessageBoxButton.OK
);
_installLocation = _installLocation.Replace(driveName, newDriveName);
_registryKey.SetValue("InstallLocation", _installLocation);
}
else if (!driveExists)
{
App.Logger.WriteLine(LOG_IDENT, $"Drive {driveName} does not exist anymore, and has likely been removed");
var result = Controls.ShowMessageBox(
$"{App.ProjectName} was originally installed to the {driveName} drive, but it appears to no longer be present. Would you like to continue and carry out a fresh install?",
MessageBoxImage.Warning,
MessageBoxButton.OKCancel
);
if (result != MessageBoxResult.OK)
App.Terminate();
FirstTimeRun();
return;
}
else
{
App.Logger.WriteLine(LOG_IDENT, "Drive has not changed, folder was likely moved or deleted");
}
}
App.BaseDirectory = _installLocation;
App.IsFirstRun = false;
}
public void Dispose()
{
_registryKey?.Dispose();
GC.SuppressFinalize(this);
}
private static void FirstTimeRun()
{
const string LOG_IDENT = "InstallChecker::FirstTimeRun";
App.Logger.WriteLine(LOG_IDENT, "Running first-time install");
App.BaseDirectory = Path.Combine(Paths.LocalAppData, App.ProjectName);
App.Logger.Initialize(true);
if (App.IsQuiet)
return;
App.IsSetupComplete = false;
App.FastFlags.Load();
Controls.ShowMenu();
// exit if we don't click the install button on installation
if (App.IsSetupComplete)
return;
App.Logger.WriteLine(LOG_IDENT, "Installation cancelled!");
App.Terminate(ErrorCode.ERROR_CANCELLED);
}
internal static void CheckUpgrade()
{
const string LOG_IDENT = "InstallChecker::CheckUpgrade";
if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application)
return;
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol
bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp"));
FileVersionInfo existingVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Application);
FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Process);
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
return;
MessageBoxResult result;
// silently upgrade version if the command line flag is set or if we're launching from an auto update
if (App.IsUpgrade || isAutoUpgrade)
{
result = MessageBoxResult.Yes;
}
else
{
result = Controls.ShowMessageBox(
$"The version of {App.ProjectName} you've launched is different to the version you currently have installed.\nWould you like to upgrade your currently installed version?",
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
}
if (result != MessageBoxResult.Yes)
return;
Filesystem.AssertReadOnly(Paths.Application);
// yes, this is EXTREMELY hacky, but the updater process that launched the
// new version may still be open and so we have to wait for it to close
int attempts = 0;
while (attempts < 10)
{
attempts++;
try
{
File.Delete(Paths.Application);
break;
}
catch (Exception)
{
if (attempts == 1)
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
Thread.Sleep(500);
}
}
if (attempts == 10)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)");
return;
}
File.Copy(Paths.Process, Paths.Application);
Bootstrapper.Register();
// update migrations
if (App.BuildMetadata.CommitRef.StartsWith("tag") && existingVersionInfo.ProductVersion == "2.4.0")
{
App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
App.FastFlags.SetValue("DFFlagVariableDPIScale2", null);
App.FastFlags.Save();
}
if (isAutoUpgrade)
{
App.NotifyIcon?.ShowAlert(
$"{App.ProjectName} has been upgraded to v{currentVersionInfo.ProductVersion}",
"See what's new in this version",
30,
(_, _) => Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/releases/tag/v{currentVersionInfo.ProductVersion}")
);
}
else if (!App.IsQuiet)
{
Controls.ShowMessageBox(
$"{App.ProjectName} has been upgraded to v{currentVersionInfo.ProductVersion}",
MessageBoxImage.Information,
MessageBoxButton.OK
);
Controls.ShowMenu();
App.Terminate();
}
}
}
}

View File

@ -1,6 +1,6 @@
namespace Bloxstrap
namespace Bloxstrap.Integrations
{
public class RobloxActivity : IDisposable
public class ActivityWatcher : IDisposable
{
// 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?
@ -11,7 +11,7 @@
private const string GameJoinedEntry = "[FLog::Network] serverId:";
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
private const string GameMessageEntry = "[FLog::Output] [SendBloxstrapMessage]";
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
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]+";
@ -24,9 +24,10 @@
public event EventHandler<string>? OnLogEntry;
public event EventHandler? OnGameJoin;
public event EventHandler? OnGameLeave;
public event EventHandler<GameMessage>? OnGameMessage;
public event EventHandler<Message>? OnRPCMessage;
private Dictionary<string, string> GeolcationCache = new();
private readonly Dictionary<string, string> GeolocationCache = new();
private DateTime LastRPCRequest;
public string LogLocation = null!;
@ -44,6 +45,8 @@
public async void StartWatcher()
{
const string LOG_IDENT = "ActivityWatcher::StartWatcher";
// okay, here's the process:
//
// - tail the latest log file from %localappdata%\roblox\logs
@ -60,7 +63,7 @@
if (App.Settings.Prop.OhHeyYouFoundMe)
delay = 250;
string logDirectory = Path.Combine(Directories.LocalAppData, "Roblox\\logs");
string logDirectory = Path.Combine(Paths.LocalAppData, "Roblox\\logs");
if (!Directory.Exists(logDirectory))
return;
@ -71,7 +74,7 @@
// if roblox doesn't start quickly enough, we can wind up fetching the previous log file
// good rule of thumb is to find a log file that was created in the last 15 seconds or so
App.Logger.WriteLine("[RobloxActivity::StartWatcher] Opening Roblox log file...");
App.Logger.WriteLine(LOG_IDENT, "Opening Roblox log file...");
while (true)
{
@ -84,13 +87,13 @@
if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now)
break;
App.Logger.WriteLine($"[RobloxActivity::StartWatcher] Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})");
App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})");
await Task.Delay(1000);
}
LogLocation = logFileInfo.FullName;
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
App.Logger.WriteLine($"[RobloxActivity::StartWatcher] Opened {LogLocation}");
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");
AutoResetEvent logUpdatedEvent = new(false);
FileSystemWatcher logWatcher = new()
@ -116,6 +119,8 @@
private void ExamineLogEntry(string entry)
{
const string LOG_IDENT = "ActivityWatcher::ExamineLogEntry";
OnLogEntry?.Invoke(this, entry);
_logEntriesRead += 1;
@ -123,9 +128,9 @@
// debug stats to ensure that the log reader is working correctly
// if more than 1000 log entries have been read, only log per 100 to save on spam
if (_logEntriesRead <= 1000 && _logEntriesRead % 50 == 0)
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Read {_logEntriesRead} log entries");
App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries");
else if (_logEntriesRead % 100 == 0)
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Read {_logEntriesRead} log entries");
App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries");
if (!ActivityInGame && ActivityPlaceId == 0)
{
@ -140,8 +145,8 @@
if (match.Groups.Count != 4)
{
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game join entry");
App.Logger.WriteLine(entry);
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
@ -162,7 +167,7 @@
_reservedTeleportMarker = false;
}
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
}
}
else if (!ActivityInGame && ActivityPlaceId != 0)
@ -173,15 +178,15 @@
if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress)
{
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game join UDMUX entry");
App.Logger.WriteLine(entry);
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join UDMUX entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
ActivityMachineAddress = match.Groups[1].Value;
ActivityMachineUDMUX = true;
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
}
else if (entry.Contains(GameJoinedEntry))
{
@ -189,12 +194,12 @@
if (match.Groups.Count != 2 || match.Groups[1].Value != ActivityMachineAddress)
{
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Failed to assert format for game joined entry");
App.Logger.WriteLine(entry);
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
ActivityInGame = true;
OnGameJoin?.Invoke(this, new EventArgs());
@ -204,7 +209,7 @@
{
if (entry.Contains(GameDisconnectedEntry))
{
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
ActivityInGame = false;
ActivityPlaceId = 0;
@ -218,7 +223,7 @@
}
else if (entry.Contains(GameTeleportingEntry))
{
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
_teleportMarker = true;
}
else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry))
@ -229,60 +234,83 @@
else if (entry.Contains(GameMessageEntry))
{
string messagePlain = entry.Substring(entry.IndexOf(GameMessageEntry) + GameMessageEntry.Length + 1);
GameMessage? message;
Message? message;
App.Logger.WriteLine($"[RobloxActivity::ExamineLogEntry] Received message: '{messagePlain}'");
App.Logger.WriteLine(LOG_IDENT, $"Received message: '{messagePlain}'");
if ((DateTime.Now - LastRPCRequest).TotalSeconds <= 1)
{
App.Logger.WriteLine(LOG_IDENT, "Dropping message as ratelimit has been hit");
return;
}
try
{
message = JsonSerializer.Deserialize<GameMessage>(messagePlain);
message = JsonSerializer.Deserialize<Message>(messagePlain);
}
catch (Exception)
{
App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (JSON deserialization threw an exception)");
App.Logger.WriteLine(LOG_IDENT, "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)");
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
if (String.IsNullOrEmpty(message.Command))
if (string.IsNullOrEmpty(message.Command))
{
App.Logger.WriteLine($"[Utilities::ExamineLogEntry] Failed to parse message! (Command is empty)");
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (Command is empty)");
return;
}
OnGameMessage?.Invoke(this, message);
OnRPCMessage?.Invoke(this, message);
LastRPCRequest = DateTime.Now;
}
}
}
public async Task<string> GetServerLocation()
{
if (GeolcationCache.ContainsKey(ActivityMachineAddress))
return GeolcationCache[ActivityMachineAddress];
const string LOG_IDENT = "ActivityWatcher::GetServerLocation";
string location = "";
if (GeolocationCache.ContainsKey(ActivityMachineAddress))
return GeolocationCache[ActivityMachineAddress];
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");
string location, locationCity, locationRegion, locationCountry = "";
try
{
locationCity = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/city");
locationRegion = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/region");
locationCountry = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{ActivityMachineAddress}/country");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {ActivityMachineAddress}");
App.Logger.WriteException(LOG_IDENT, ex);
return "N/A (lookup failed)";
}
locationCity = locationCity.ReplaceLineEndings("");
locationRegion = locationRegion.ReplaceLineEndings("");
locationCountry = locationCountry.ReplaceLineEndings("");
if (String.IsNullOrEmpty(locationCountry))
if (string.IsNullOrEmpty(locationCountry))
location = "N/A";
else if (locationCity == locationRegion)
location = $"{locationRegion}, {locationCountry}";
else
location = $"{locationCity}, {locationRegion}, {locationCountry}";
GeolcationCache[ActivityMachineAddress] = location;
if (!ActivityInGame)
return "N/A (left game)";
GeolocationCache[ActivityMachineAddress] = location;
return location;
}

View File

@ -5,94 +5,158 @@ namespace Bloxstrap.Integrations
public class DiscordRichPresence : IDisposable
{
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
private readonly RobloxActivity _activityWatcher;
private readonly ActivityWatcher _activityWatcher;
private DiscordRPC.RichPresence? _currentPresence;
private DiscordRPC.RichPresence? _currentPresenceCopy;
private RichPresence? _currentPresence;
private bool _visible = true;
private string? _initialStatus;
private long _currentUniverseId;
private DateTime? _timeStartedUniverse;
public DiscordRichPresence(RobloxActivity activityWatcher)
public DiscordRichPresence(ActivityWatcher activityWatcher)
{
const string LOG_IDENT = "DiscordRichPresence::DiscordRichPresence";
_activityWatcher = activityWatcher;
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameMessage += (_, message) => OnGameMessage(message);
_activityWatcher.OnRPCMessage += (_, message) => ProcessRPCMessage(message);
_rpcClient.OnReady += (_, e) =>
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})");
App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})");
_rpcClient.OnPresenceUpdate += (_, e) =>
App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Presence updated");
App.Logger.WriteLine(LOG_IDENT, "Presence updated");
_rpcClient.OnError += (_, e) =>
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] An RPC error occurred - {e.Message}");
App.Logger.WriteLine(LOG_IDENT, $"An RPC error occurred - {e.Message}");
_rpcClient.OnConnectionEstablished += (_, e) =>
App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Established connection with Discord RPC");
App.Logger.WriteLine(LOG_IDENT, "Established connection with Discord RPC");
//spams log as it tries to connect every ~15 sec when discord is closed so not now
//_rpcClient.OnConnectionFailed += (_, e) =>
// App.Logger.WriteLine("[DiscordRichPresence::DiscordRichPresence] Failed to establish connection with Discord RPC");
// App.Logger.WriteLine(LOG_IDENT, "Failed to establish connection with Discord RPC");
_rpcClient.OnClose += (_, e) =>
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Lost connection to Discord RPC - {e.Reason} ({e.Code})");
App.Logger.WriteLine(LOG_IDENT, $"Lost connection to Discord RPC - {e.Reason} ({e.Code})");
_rpcClient.Initialize();
}
public void OnGameMessage(GameMessage message)
public void ProcessRPCMessage(Message message)
{
if (message.Command == "SetPresenceStatus")
SetStatus(message.Data);
}
const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage";
public void SetStatus(string status)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Setting status to '{status}'");
if (message.Command != "SetRichPresence")
return;
if (_currentPresence is null)
if (_currentPresence is null || _currentPresenceCopy is null)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Presence is not set, aborting");
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, aborting");
return;
}
if (status.Length > 128)
Models.BloxstrapRPC.RichPresence? presenceData;
// a lot of repeated code here, could this somehow be cleaned up?
try
{
App.Logger.WriteLine($"[DiscordRichPresence::SetStatus] Status cannot be longer than 128 characters, aborting");
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
}
catch (Exception)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
return;
}
if (_initialStatus is null)
_initialStatus = _currentPresence.State;
string finalStatus;
if (string.IsNullOrEmpty(status))
if (presenceData is null)
{
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");
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
_currentPresence.State = finalStatus;
if (presenceData.Details is not null)
{
if (presenceData.Details.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
else if (presenceData.Details == "<reset>")
_currentPresence.Details = _currentPresenceCopy.Details;
else
_currentPresence.Details = presenceData.Details;
}
if (presenceData.State is not null)
{
if (presenceData.State.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
else if (presenceData.State == "<reset>")
_currentPresence.State = _currentPresenceCopy.State;
else
_currentPresence.State = presenceData.State;
}
if (presenceData.TimestampStart == 0)
_currentPresence.Timestamps.Start = null;
else if (presenceData.TimestampStart is not null)
_currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
if (presenceData.TimestampEnd == 0)
_currentPresence.Timestamps.End = null;
else if (presenceData.TimestampEnd is not null)
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
if (presenceData.SmallImage is not null)
{
if (presenceData.SmallImage.Clear)
{
_currentPresence.Assets.SmallImageKey = "";
}
else if (presenceData.SmallImage.Reset)
{
_currentPresence.Assets.SmallImageText = _currentPresenceCopy.Assets.SmallImageText;
_currentPresence.Assets.SmallImageKey = _currentPresenceCopy.Assets.SmallImageKey;
}
else
{
if (presenceData.SmallImage.AssetId is not null)
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
if (presenceData.SmallImage.HoverText is not null)
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
}
}
if (presenceData.LargeImage is not null)
{
if (presenceData.LargeImage.Clear)
{
_currentPresence.Assets.LargeImageKey = "";
}
else if (presenceData.LargeImage.Reset)
{
_currentPresence.Assets.LargeImageText = _currentPresenceCopy.Assets.LargeImageText;
_currentPresence.Assets.LargeImageKey = _currentPresenceCopy.Assets.LargeImageKey;
}
else
{
if (presenceData.LargeImage.AssetId is not null)
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
if (presenceData.LargeImage.HoverText is not null)
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
}
}
UpdatePresence();
}
public void SetVisibility(bool visible)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetVisibility] Setting presence visibility ({visible})");
App.Logger.WriteLine("DiscordRichPresence::SetVisibility", $"Setting presence visibility ({visible})");
_visible = visible;
@ -104,11 +168,12 @@ namespace Bloxstrap.Integrations
public async Task<bool> SetCurrentGame()
{
const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame";
if (!_activityWatcher.ActivityInGame)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Not in game, clearing presence");
_currentPresence = null;
_initialStatus = null;
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
_currentPresence = _currentPresenceCopy = null;
UpdatePresence();
return true;
}
@ -116,17 +181,17 @@ namespace Bloxstrap.Integrations
string icon = "roblox";
long placeId = _activityWatcher.ActivityPlaceId;
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Setting presence for Place ID {placeId}");
App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}");
var universeIdResponse = await Http.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{placeId}/universe");
if (universeIdResponse is null)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe ID!");
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe ID!");
return false;
}
long universeId = universeIdResponse.UniverseId;
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe ID as {universeId}");
App.Logger.WriteLine(LOG_IDENT, $"Got Universe ID as {universeId}");
// preserve time spent playing if we're teleporting between places in the same universe
if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId)
@ -137,22 +202,22 @@ namespace Bloxstrap.Integrations
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeId}");
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
{
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe info!");
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe info!");
return false;
}
GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0];
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe details");
App.Logger.WriteLine(LOG_IDENT, "Got Universe details");
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())
{
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Could not get Universe thumbnail info!");
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe thumbnail info!");
}
else
{
icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl;
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Got Universe thumbnail as {icon}");
App.Logger.WriteLine(LOG_IDENT, $"Got Universe thumbnail as {icon}");
}
List<Button> buttons = new();
@ -174,7 +239,7 @@ namespace Bloxstrap.Integrations
if (!_activityWatcher.ActivityInGame || placeId != _activityWatcher.ActivityPlaceId)
{
App.Logger.WriteLine($"[DiscordRichPresence::SetCurrentGame] Aborting presence set because game activity has changed");
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
return false;
}
@ -185,7 +250,7 @@ namespace Bloxstrap.Integrations
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""),
};
_currentPresence = new RichPresence
_currentPresence = new DiscordRPC.RichPresence
{
Details = $"Playing {universeDetails.Name}",
State = status,
@ -200,6 +265,9 @@ namespace Bloxstrap.Integrations
}
};
// this is used for configuration from BloxstrapRPC
_currentPresenceCopy = _currentPresence.Clone();
UpdatePresence();
return true;
@ -207,14 +275,16 @@ namespace Bloxstrap.Integrations
public void UpdatePresence()
{
const string LOG_IDENT = "DiscordRichPresence::UpdatePresence";
if (_currentPresence is null)
{
App.Logger.WriteLine($"[DiscordRichPresence::UpdatePresence] Presence is empty, clearing");
App.Logger.WriteLine(LOG_IDENT, $"Presence is empty, clearing");
_rpcClient.ClearPresence();
return;
}
App.Logger.WriteLine($"[DiscordRichPresence::UpdatePresence] Updating presence");
App.Logger.WriteLine(LOG_IDENT, $"Updating presence");
if (_visible)
_rpcClient.SetPresence(_currentPresence);
@ -222,7 +292,7 @@ namespace Bloxstrap.Integrations
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.Dispose();
GC.SuppressFinalize(this);

View File

@ -1,13 +1,19 @@
namespace Bloxstrap
using System.Runtime.CompilerServices;
namespace Bloxstrap
{
public class JsonManager<T> where T : new()
{
public T Prop { get; set; } = new();
public virtual string FileLocation => Path.Combine(Directories.Base, $"{typeof(T).Name}.json");
public virtual string FileLocation => Path.Combine(Paths.Base, $"{typeof(T).Name}.json");
private string LOG_IDENT_CLASS => $"JsonManager<{typeof(T).Name}>";
public virtual void Load()
{
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Loading from {FileLocation}...");
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load";
App.Logger.WriteLine(LOG_IDENT, $"Loading from {FileLocation}...");
try
{
@ -18,28 +24,31 @@
Prop = settings;
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Loaded successfully!");
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
}
catch (Exception ex)
{
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Load] Failed to load! ({ex.Message})");
App.Logger.WriteLine(LOG_IDENT, "Failed to load!");
App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}");
}
}
public virtual void Save()
{
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Save";
if (!App.ShouldSaveConfigs)
{
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Save request ignored");
App.Logger.WriteLine(LOG_IDENT, "Save request ignored");
return;
}
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Saving to {FileLocation}...");
App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}...");
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
App.Logger.WriteLine($"[JsonManager<{typeof(T).Name}>::Save] Save complete!");
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
}
}
}

View File

@ -1,10 +1,6 @@
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
{
@ -17,16 +13,18 @@
public void Initialize(bool useTempDir = false)
{
string directory = useTempDir ? Path.Combine(Directories.LocalAppData, "Temp") : Path.Combine(Directories.Base, "Logs");
const string LOG_IDENT = "Logger::Initialize";
string directory = useTempDir ? Path.Combine(Paths.LocalAppData, "Temp") : Path.Combine(Paths.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}");
WriteLine(LOG_IDENT, $"Initializing at {location}");
if (Initialized)
{
WriteLine("[Logger::Initialize] Failed to initialize because logger is already initialized");
WriteLine(LOG_IDENT, "Failed to initialize because logger is already initialized");
return;
}
@ -34,45 +32,63 @@
if (File.Exists(location))
{
WriteLine("[Logger::Initialize] Failed to initialize because log file already exists");
WriteLine(LOG_IDENT, "Failed to initialize because log file already exists");
return;
}
_filestream = File.Open(location, FileMode.Create, FileAccess.Write, FileShare.Read);
try
{
_filestream = File.Open(location, FileMode.Create, FileAccess.Write, FileShare.Read);
}
catch (IOException)
{
WriteLine(LOG_IDENT, "Failed to initialize because log file already exists");
}
Initialized = true;
if (Backlog.Count > 0)
WriteToLog(string.Join("\r\n", Backlog));
WriteLine($"[Logger::Initialize] Finished initializing!");
WriteLine(LOG_IDENT, "Finished initializing!");
FileLocation = location;
// clean up any logs older than a week
if (Directories.Initialized && Directory.Exists(Directories.Logs))
if (Paths.Initialized && Directory.Exists(Paths.Logs))
{
foreach (FileInfo log in new DirectoryInfo(Directories.Logs).GetFiles())
foreach (FileInfo log in new DirectoryInfo(Paths.Logs).GetFiles())
{
if (log.LastWriteTimeUtc.AddDays(7) > DateTime.UtcNow)
continue;
App.Logger.WriteLine($"[Logger::Initialize] Cleaning up old log file '{log.Name}'");
WriteLine(LOG_IDENT, $"Cleaning up old log file '{log.Name}'");
log.Delete();
}
}
}
public void WriteLine(string message)
private void WriteLine(string message)
{
string timestamp = DateTime.UtcNow.ToString("s") + "Z";
string outcon = $"{timestamp} {message}";
string outlog = outcon.Replace(Directories.UserProfile, "%UserProfile%");
string outlog = outcon.Replace(Paths.UserProfile, "%UserProfile%");
Debug.WriteLine(outcon);
WriteToLog(outlog);
}
public void WriteLine(string identifier, string message) => WriteLine($"[{identifier}] {message}");
public void WriteException(string identifier, Exception ex)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
WriteLine($"[{identifier}] {ex}");
}
private async void WriteToLog(string message)
{
if (!Initialized)
@ -84,8 +100,9 @@
try
{
await _semaphore.WaitAsync();
await _filestream!.WriteAsync(Encoding.Unicode.GetBytes($"{message}\r\n"));
await _filestream.FlushAsync();
await _filestream!.WriteAsync(Encoding.UTF8.GetBytes($"{message}\r\n"));
_ = _filestream.FlushAsync();
}
finally
{

View File

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

View File

@ -0,0 +1,23 @@
namespace Bloxstrap.Models.BloxstrapRPC
{
class RichPresence
{
[JsonPropertyName("details")]
public string? Details { get; set; }
[JsonPropertyName("state")]
public string? State { get; set; }
[JsonPropertyName("timeStart")]
public ulong? TimestampStart { get; set; }
[JsonPropertyName("timeEnd")]
public ulong? TimestampEnd { get; set; }
[JsonPropertyName("smallImage")]
public RichPresenceImage? SmallImage { get; set; }
[JsonPropertyName("largeImage")]
public RichPresenceImage? LargeImage { get; set; }
}
}

View File

@ -0,0 +1,17 @@
namespace Bloxstrap.Models.BloxstrapRPC
{
class RichPresenceImage
{
[JsonPropertyName("assetId")]
public ulong? AssetId { get; set; }
[JsonPropertyName("hoverText")]
public string? HoverText { get; set; }
[JsonPropertyName("clear")]
public bool Clear { get; set; } = false;
[JsonPropertyName("reset")]
public bool Reset { get; set; } = false;
}
}

View File

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

View File

@ -0,0 +1,5 @@
SetForegroundWindow
FlashWindow
GetWindowLong
SetWindowLong
EnumDisplaySettings

View File

@ -1,6 +1,6 @@
namespace Bloxstrap
{
static class Directories
static class Paths
{
// note that these are directories that aren't tethered to the basedirectory
// so these can safely be called before initialization
@ -8,7 +8,9 @@
public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
public static string StartMenu => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", App.ProjectName);
public static string MyPictures => Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
public static string System => Environment.GetFolderPath(Environment.SpecialFolder.System);
public static string Process => Environment.ProcessPath!;
public static string Base { get; private set; } = "";
public static string Downloads { get; private set; } = "";
@ -19,6 +21,8 @@
public static string Application { get; private set; } = "";
public static string CustomFont => Path.Combine(Modifications, "content\\fonts\\CustomFont.ttf");
public static bool Initialized => !String.IsNullOrEmpty(Base);
public static void Initialize(string baseDirectory)

View File

@ -94,7 +94,7 @@ namespace Bloxstrap
return;
}
App.Logger.WriteLine($"[Protocol::ParseUri] Changed Roblox build channel from {App.Settings.Prop.Channel} to {channel}");
App.Logger.WriteLine("Protocol::ParseUri", $"Changed Roblox channel from {App.Settings.Prop.Channel} to {channel}");
App.Settings.Prop.Channel = channel;
}
@ -137,7 +137,7 @@ namespace Bloxstrap
}
catch (Exception ex)
{
App.Logger.WriteLine($"[Protocol::Unregister] Failed to unregister {key}: {ex}");
App.Logger.WriteLine("Protocol::Unregister", $"Failed to unregister {key}: {ex}");
}
}
}

View File

@ -1,6 +1,4 @@
using Bloxstrap.Exceptions;
namespace Bloxstrap
namespace Bloxstrap
{
public static class RobloxDeployment
{
@ -23,24 +21,26 @@ namespace Bloxstrap
{
get
{
const string LOG_IDENT = "DeployManager::DefaultBaseUrl.Set";
if (string.IsNullOrEmpty(_baseUrl))
{
// check for a working accessible deployment domain
foreach (string attemptedUrl in BaseUrls)
{
App.Logger.WriteLine($"[DeployManager::DefaultBaseUrl.Set] Testing connection to '{attemptedUrl}'...");
App.Logger.WriteLine(LOG_IDENT, $"Testing connection to '{attemptedUrl}'...");
try
{
App.HttpClient.GetAsync($"{attemptedUrl}/version").Wait();
App.Logger.WriteLine($"[DeployManager::DefaultBaseUrl.Set] Connection successful!");
App.Logger.WriteLine(LOG_IDENT, "Connection successful!");
_baseUrl = attemptedUrl;
break;
}
catch (Exception ex)
{
App.Logger.WriteLine($"[DeployManager::DefaultBaseUrl.Set] Connection failed!");
App.Logger.WriteLine($"[DeployManager::DefaultBaseUrl.Set] {ex}");
App.Logger.WriteLine(LOG_IDENT, "Connection failed!");
App.Logger.WriteException(LOG_IDENT, ex);
continue;
}
}
@ -83,18 +83,33 @@ namespace Bloxstrap
public static async Task<ClientVersion> GetInfo(string channel, bool extraInformation = false)
{
App.Logger.WriteLine($"[RobloxDeployment::GetInfo] Getting deploy info for channel {channel} (extraInformation={extraInformation})");
const string LOG_IDENT = "RobloxDeployment::GetInfo";
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel} (extraInformation={extraInformation})");
ClientVersion clientVersion;
if (ClientVersionCache.ContainsKey(channel))
{
App.Logger.WriteLine($"[RobloxDeployment::GetInfo] Deploy information is cached");
App.Logger.WriteLine(LOG_IDENT, "Deploy information is cached");
clientVersion = ClientVersionCache[channel];
}
else
{
HttpResponseMessage deployInfoResponse = await App.HttpClient.GetAsync($"https://clientsettingscdn.roblox.com/v2/client-version/WindowsPlayer/channel/{channel}");
string path = $"/v2/client-version/WindowsPlayer/channel/{channel}";
HttpResponseMessage deployInfoResponse;
try
{
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path);
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");
App.Logger.WriteException(LOG_IDENT, ex);
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettings.roblox.com" + path);
}
string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync();
@ -105,23 +120,31 @@ namespace Bloxstrap
// 500 = Error while fetching version information.
// either way, we throw
App.Logger.WriteLine(
"[RobloxDeployment::GetInfo] Failed to fetch deploy info!\r\n" +
App.Logger.WriteLine(LOG_IDENT,
"Failed to fetch deploy info!\r\n" +
$"\tStatus code: {deployInfoResponse.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseUnsuccessfulException(deployInfoResponse);
throw new HttpResponseException(deployInfoResponse);
}
clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawResponse)!;
}
// 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;
}
// for preferences
if (extraInformation && clientVersion.Timestamp is null)
{
App.Logger.WriteLine("[RobloxDeployment::GetInfo] Getting extra information...");
App.Logger.WriteLine(LOG_IDENT, "Getting extra information...");
string manifestUrl = GetLocation($"/{clientVersion.VersionGuid}-rbxPkgManifest.txt", channel);
@ -131,18 +154,9 @@ namespace Bloxstrap
if (pkgResponse.Content.Headers.TryGetValues("last-modified", out var values))
{
string lastModified = values.First();
App.Logger.WriteLine($"[RobloxDeployment::GetInfo] {manifestUrl} - Last-Modified: {lastModified}");
App.Logger.WriteLine(LOG_IDENT, $"{manifestUrl} - Last-Modified: {lastModified}");
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;

View File

@ -39,6 +39,14 @@ namespace Bloxstrap.UI
});
}
public static void ShowConnectivityDialog(string targetName, string description, Exception exception)
{
Application.Current.Dispatcher.Invoke(() =>
{
new ConnectivityDialog(targetName, description, exception).ShowDialog();
});
}
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
{
return style switch

View File

@ -11,6 +11,7 @@
ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
@ -37,7 +38,10 @@
</StackPanel>
</Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<Border Grid.Row="2" Padding="15">
<Border.Background>
<SolidColorBrush Color="{ui:ThemeResource SolidBackgroundFillColorSecondary}" Opacity="{Binding FooterOpacity, Mode=OneTime}" />
</Border.Background>
<Button Margin="0" Content="Cancel" Width="120" HorizontalAlignment="Right" IsEnabled="{Binding CancelEnabled, Mode=OneWay}" Command="{Binding CancelInstallCommand}" />
</Border>
</Grid>

View File

@ -66,7 +66,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
public FluentDialog()
{
_viewModel = new BootstrapperDialogViewModel(this);
_viewModel = new FluentDialogViewModel(this);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();

View File

@ -1,6 +1,7 @@
using System.Windows;
using System.Windows.Controls;
using Bloxstrap.Integrations;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
@ -12,7 +13,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{
private bool _autoscroll = true;
public LogTracer(RobloxActivity activityWatcher)
public LogTracer(ActivityWatcher activityWatcher)
{
DataContext = new LogTracerViewModel(this, activityWatcher);
InitializeComponent();

View File

@ -21,11 +21,44 @@
Closed="Window_Closed">
<ui:UiWindow.ContextMenu>
<ContextMenu>
<MenuItem x:Name="VersionMenuItem" IsEnabled="False" />
<MenuItem IsEnabled="False">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="pack://application:,,,/Bloxstrap.ico" />
<TextBlock x:Name="VersionTextBlock" Foreground="{DynamicResource TextFillColorSecondaryBrush}" Grid.Column="1" VerticalAlignment="Center" Margin="6,0,0,0" Text="Bloxstrap v2.4.1" />
</Grid>
</MenuItem.Header>
</MenuItem>
<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="InviteDeeplinkMenuItem" Visibility="Collapsed" Click="InviteDeeplinkMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="ClipboardLink24"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="Copy invite deeplink" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="ServerDetailsMenuItem" Visibility="Collapsed" Click="ServerDetailsMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="Info28"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="See server details" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="LogTracerMenuItem" Header="Open log tracer" Visibility="Collapsed" Click="LogTracerMenuItem_Click" />
</ContextMenu>
</ui:UiWindow.ContextMenu>

View File

@ -1,17 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
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 Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using Bloxstrap.Integrations;
@ -24,13 +17,13 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{
// 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 ActivityWatcher? _activityWatcher;
private readonly DiscordRichPresence? _richPresenceHandler;
private LogTracer? _logTracerWindow;
private ServerInformation? _serverInformationWindow;
public MenuContainer(RobloxActivity? activityWatcher, DiscordRichPresence? richPresenceHandler)
public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler)
{
InitializeComponent();
@ -49,7 +42,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
if (_richPresenceHandler is not null)
RichPresenceMenuItem.Visibility = Visibility.Visible;
VersionMenuItem.Header = $"{App.ProjectName} v{App.Version}";
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
}
public void ShowServerInformationWindow()
@ -92,13 +85,14 @@ namespace Bloxstrap.UI.Elements.ContextMenu
// 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);
HWND hWnd = (HWND)new WindowInteropHelper(this).Handle;
int exStyle = PInvoke.GetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
exStyle |= 0x00000080; //NativeMethods.WS_EX_TOOLWINDOW;
PInvoke.SetWindowLong(hWnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, exStyle);
}
private void Window_Closed(object sender, EventArgs e) => App.Logger.WriteLine("[MenuContainer::Window_Closed] Context menu container closed");
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);

View File

@ -12,6 +12,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Bloxstrap.Integrations;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
@ -21,7 +22,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
/// </summary>
public partial class ServerInformation
{
public ServerInformation(RobloxActivity activityWatcher)
public ServerInformation(ActivityWatcher activityWatcher)
{
DataContext = new ServerInformationViewModel(this, activityWatcher);
InitializeComponent();

View File

@ -0,0 +1,44 @@
<ui:UiWindow x:Class="Bloxstrap.UI.Elements.Dialogs.ConnectivityDialog"
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" Title="Connectivity error" />
<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 x:Name="TitleTextBlock" Text="? is unable to connect to ?" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock" Text="?" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="More information:" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,8,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" />
</StackPanel>
</Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button x:Name="CloseButton" MinWidth="100" Content="Close" Margin="12,0,0,0" />
</StackPanel>
</Border>
</Grid>
</ui:UiWindow>

View File

@ -0,0 +1,45 @@
using System.Media;
using System.Windows.Interop;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Bloxstrap.UI.Elements.Dialogs
{
// hmm... do i use MVVM for this?
// this is entirely static, so i think im fine without it, and this way is just so much more efficient
/// <summary>
/// Interaction logic for ExceptionDialog.xaml
/// </summary>
public partial class ConnectivityDialog
{
public ConnectivityDialog(string targetName, string description, Exception exception)
{
Exception? innerException = exception.InnerException;
InitializeComponent();
TitleTextBlock.Text = $"{App.ProjectName} is unable to connect to {targetName}";
DescriptionTextBlock.Text = description;
ErrorRichTextBox.Selection.Text = $"{exception.GetType()}: {exception.Message}";
if (innerException is not null)
ErrorRichTextBox.Selection.Text += $"\n\n===== Inner Exception =====\n{innerException.GetType()}: {innerException.Message}";
CloseButton.Click += delegate
{
Close();
};
SystemSounds.Hand.Play();
Loaded += delegate
{
var hWnd = new WindowInteropHelper(this).Handle;
PInvoke.FlashWindow((HWND)hWnd, true);
};
}
}
}

View File

@ -2,6 +2,9 @@
using System.Windows;
using System.Windows.Interop;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Bloxstrap.UI.Elements.Dialogs
{
// hmm... do i use MVVM for this?
@ -60,7 +63,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
Loaded += delegate
{
IntPtr hWnd = new WindowInteropHelper(this).Handle;
NativeMethods.FlashWindow(hWnd, true);
PInvoke.FlashWindow((HWND)hWnd, true);
};
}
}

View File

@ -4,6 +4,9 @@ using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Windows.Win32;
using Windows.Win32.Foundation;
using Bloxstrap.UI.Utility;
namespace Bloxstrap.UI.Elements.Dialogs
@ -107,8 +110,8 @@ namespace Bloxstrap.UI.Elements.Dialogs
Loaded += delegate
{
IntPtr hWnd = new WindowInteropHelper(this).Handle;
NativeMethods.FlashWindow(hWnd, true);
var hWnd = new WindowInteropHelper(this).Handle;
PInvoke.FlashWindow((HWND)hWnd, true);
};
}

View File

@ -19,7 +19,7 @@ namespace Bloxstrap.UI.Elements.Menu
public MainWindow()
{
App.Logger.WriteLine("[MainWindow::MainWindow] Initializing menu");
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu");
DataContext = new MainWindowViewModel(this, _dialogService);
SetTheme();

View File

@ -4,115 +4,177 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Menu.Pages"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Menu"
d:DataContext="{d:DesignInstance Type=models:BehaviourViewModel}"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800"
Title="BehaviourPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<TextBlock Margin="0,0,0,8" Text="Configure what Bloxstrap should do when launching." FontSize="14" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Create desktop icon" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Bloxstrap will place an icon on the desktop that launches Roblox the next time it launches." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ui:ToggleSwitch IsChecked="{Binding CreateDesktopIcon, Mode=TwoWay}" />
</ui:CardControl>
<StackPanel Margin="0,0,14,14">
<TextBlock Margin="0,0,0,8" Text="Configure what Bloxstrap should do when launching." FontSize="14" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Automatically update Bloxstrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Bloxstrap will automatically check and update itself when launching Roblox." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ui:ToggleSwitch IsChecked="{Binding UpdateCheckingEnabled, Mode=TwoWay}" />
</ui:CardControl>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Create desktop icon" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Bloxstrap will place an icon on the desktop that launches Roblox the next time it launches." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ui:ToggleSwitch IsChecked="{Binding CreateDesktopIcon, Mode=TwoWay}" />
</ui:CardControl>
<ui:CardExpander Margin="0,8,0,0" IsExpanded="True">
<ui:CardExpander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock FontSize="14" Text="Channel" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Choose which deployment channel Roblox should be downloaded from." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
<ComboBox Grid.Column="1" Margin="8,0,8,0" Padding="10,5,10,5" Width="200" IsEditable="True" ItemsSource="{Binding Channels, Mode=OneWay}" Text="{Binding SelectedChannel, Mode=TwoWay, Delay=250}" />
</Grid>
</ui:CardExpander.Header>
<StackPanel>
<Grid Margin="0,0,4,0">
<Grid.Style>
<Style>
<Setter Property="Grid.Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ChannelDeployInfo}" Value="{x:Null}">
<Setter Property="Grid.Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Automatically update Bloxstrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Bloxstrap will automatically check and update itself when launching Roblox." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ui:ToggleSwitch IsChecked="{Binding UpdateCheckingEnabled, Mode=TwoWay}" />
</ui:CardControl>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,16,8" VerticalAlignment="Center" Text="Version" />
<TextBlock Grid.Row="0" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ChannelDeployInfo.Version, Mode=OneWay}" />
<ui:CardExpander Margin="0,8,0,0" IsExpanded="True">
<ui:CardExpander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock FontSize="14" Text="Channel" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Choose which deployment channel Roblox should be downloaded from." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
<ComboBox Grid.Column="1" Margin="8,0,8,0" Padding="10,5,10,5" Width="200" IsEditable="True" ItemsSource="{Binding Channels, Mode=OneWay}" Text="{Binding SelectedChannel, Mode=TwoWay, Delay=250}" />
</Grid>
</ui:CardExpander.Header>
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,16,8" VerticalAlignment="Center" Text="VersionGuid" />
<TextBlock Grid.Row="1" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ChannelDeployInfo.VersionGuid, Mode=OneWay}" />
<StackPanel>
<Grid Margin="0,0,4,0">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ChannelDeployInfo}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,0" VerticalAlignment="Center" Text="Deployed" />
<TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ChannelDeployInfo.Timestamp, Mode=OneWay}" />
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="3" Grid.ColumnSpan="2" Margin="0,16,0,0" Orientation="Horizontal" Visibility="{Binding ChannelWarningVisibility, Mode=OneWay}">
<Image Grid.Column="0" Width="24" Height="24" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Warning.png" />
<TextBlock Margin="8,0,0,0" VerticalAlignment="Center" Text="This channel is out of date, and is likely no longer being updated. Please use another channel." />
</StackPanel>
</Grid>
<Grid Column="0">
<Grid.Style>
<Style>
<Setter Property="Grid.Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ChannelDeployInfo}" Value="{x:Null}">
<Setter Property="Grid.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:ProgressRing Grid.Column="0" Margin="6" IsIndeterminate="True" Visibility="{Binding LoadingSpinnerVisibility, Mode=OneWay}" />
<Image Grid.Column="0" Margin="6" Width="60" Height="60" Visibility="{Binding LoadingErrorVisibility, Mode=OneWay}" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Error.png" />
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" Margin="16" VerticalAlignment="Center" Text="{Binding ChannelInfoLoadingText, Mode=OneWay}" TextWrapping="Wrap" />
</Grid>
</StackPanel>
</ui:CardExpander>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,16,8" VerticalAlignment="Center" Text="Version" />
<TextBlock Grid.Row="0" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ChannelDeployInfo.Version, Mode=OneWay}" />
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Automatic channel change action" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Roblox or Bloxstrap may try to change your preferred channel." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding ChannelChangeModes.Keys, Mode=OneTime}" Text="{Binding SelectedChannelChangeMode, Mode=TwoWay}" />
</ui:CardControl>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,16,8" VerticalAlignment="Center" Text="VersionGuid" />
<TextBlock Grid.Row="1" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ChannelDeployInfo.VersionGuid, Mode=OneWay}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,0" VerticalAlignment="Center" Text="Deployed" />
<TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ChannelDeployInfo.Timestamp, Mode=OneWay}" />
<StackPanel Grid.Row="3" Grid.ColumnSpan="2" Margin="0,16,0,0" Orientation="Horizontal">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ShowChannelWarning, Mode=OneWay}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<Image Grid.Column="0" Width="24" Height="24" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Warning.png" />
<TextBlock Margin="8,0,0,0" VerticalAlignment="Center" Text="This channel is out of date, and is likely no longer being updated. Please use another channel." />
</StackPanel>
</Grid>
<Grid Column="0">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ChannelDeployInfo}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:ProgressRing Grid.Column="0" Margin="6" IsIndeterminate="True">
<ui:ProgressRing.Style>
<Style TargetType="ui:ProgressRing" BasedOn="{StaticResource {x:Type ui:ProgressRing}}">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding ShowLoadingError, Mode=OneWay}" Value="True">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ui:ProgressRing.Style>
</ui:ProgressRing>
<Image Grid.Column="0" Margin="6" Width="60" Height="60" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Error.png">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ShowLoadingError, Mode=OneWay}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Grid.Column="1" Margin="16" VerticalAlignment="Center" Text="{Binding ChannelInfoLoadingText, Mode=OneWay}" TextWrapping="Wrap" />
</Grid>
</StackPanel>
</ui:CardExpander>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Automatic channel change action" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Roblox or Bloxstrap may try to change your preferred channel." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding ChannelChangeModes.Keys, Mode=OneTime}" Text="{Binding SelectedChannelChangeMode, Mode=TwoWay}" />
</ui:CardControl>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Style>
<Style TargetType="ui:CardControl" BasedOn="{StaticResource {x:Type ui:CardControl}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ForceRobloxReinstallation, Mode=OneTime}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ui:CardControl.Style>
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Force Roblox reinstallation" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Roblox will be installed fresh on next launch." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ui:ToggleSwitch IsChecked="{Binding ForceRobloxReinstallation, Mode=TwoWay}" />
</ui:CardControl>
</StackPanel>
</ui:UiPage>

View File

@ -1,4 +1,18 @@
using Bloxstrap.UI.ViewModels.Menu;
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.Navigation;
using System.Windows.Shapes;
namespace Bloxstrap.UI.Elements.Menu.Pages
{
@ -12,5 +26,10 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
DataContext = new BehaviourViewModel();
InitializeComponent();
}
private void ToggleSwitch_Checked(object sender, RoutedEventArgs e)
{
}
}
}

View File

@ -11,6 +11,7 @@
Loaded="Page_Loaded">
<Grid Margin="0,0,14,14">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@ -33,9 +34,12 @@
</ui:Button.Style>
</ui:Button>
<ToggleButton x:Name="TogglePresetsButton" Content="Show preset flags" Click="ToggleButton_Click" Margin="12,0,0,0" />
<ui:Button Icon="ArrowImport24" Content="Import JSON" Margin="12,0,0,0" Click="ImportJSONButton_Click" />
</StackPanel>
<DataGrid Name="DataGrid" Grid.Row="2" HeadersVisibility="Column" GridLinesVisibility="Horizontal" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CellEditEnding="DataGrid_CellEditEnding">
<ui:TextBox x:Name="SearchTextBox" Grid.Row="2" Margin="0,0,0,16" Icon="Search32" PlaceholderText="Search" TextChanged="SearchTextBox_TextChanged" />
<DataGrid Name="DataGrid" Grid.Row="3" HeadersVisibility="Column" GridLinesVisibility="Horizontal" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CellEditEnding="DataGrid_CellEditEnding">
<DataGrid.Style>
<Style TargetType="DataGrid" BasedOn="{StaticResource {x:Type DataGrid}}">
<Setter Property="Background" Value="Transparent" />

View File

@ -3,6 +3,8 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Microsoft.Win32;
using Wpf.Ui.Mvvm.Contracts;
using Bloxstrap.UI.Elements.Dialogs;
@ -19,6 +21,7 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
private readonly ObservableCollection<FastFlag> _fastFlagList = new();
private bool _showPresets = false;
private string _searchFilter = "";
public FastFlagEditorPage()
{
@ -33,11 +36,14 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
var presetFlags = FastFlagManager.PresetFlags.Values;
foreach (var pair in App.FastFlags.Prop)
foreach (var pair in App.FastFlags.Prop.OrderBy(x => x.Key))
{
if (!_showPresets && presetFlags.Contains(pair.Key))
continue;
if (!pair.Key.ToLower().Contains(_searchFilter.ToLower()))
continue;
var entry = new FastFlag
{
// Enabled = true,
@ -69,6 +75,15 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
DataGrid.ScrollIntoView(newSelectedEntry);
}
private void ClearSearch(bool refresh = true)
{
SearchTextBox.Text = "";
_searchFilter = "";
if (refresh)
ReloadList();
}
// refresh list on page load to synchronize with preset page
private void Page_Loaded(object sender, RoutedEventArgs e) => ReloadList();
@ -139,6 +154,9 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
Value = dialog.FlagValueTextBox.Text
};
if (!name.Contains(_searchFilter))
ClearSearch();
_fastFlagList.Add(entry);
App.FastFlags.SetValue(entry.Name, entry.Value);
@ -147,13 +165,24 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
{
Controls.ShowMessageBox("An entry for this FastFlag already exists.", MessageBoxImage.Information);
if (!_showPresets && FastFlagManager.PresetFlags.Values.Contains(dialog.FlagNameTextBox.Text))
bool refresh = false;
if (!_showPresets && FastFlagManager.PresetFlags.Values.Contains(name))
{
_showPresets = true;
TogglePresetsButton.IsChecked = true;
ReloadList();
_showPresets = true;
refresh = true;
}
if (!name.Contains(_searchFilter))
{
ClearSearch(false);
refresh = true;
}
if (refresh)
ReloadList();
entry = _fastFlagList.Where(x => x.Name == name).FirstOrDefault();
}
@ -183,5 +212,70 @@ namespace Bloxstrap.UI.Elements.Menu.Pages
_showPresets = button.IsChecked ?? false;
ReloadList();
}
private void ImportJSONButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Filter = "JSON files|*.json|All files|*.*"
};
if (dialog.ShowDialog() != true)
return;
try
{
var list = JsonSerializer.Deserialize<Dictionary<string, object>>(File.ReadAllText(dialog.FileName));
if (list is null)
throw new Exception("JSON deserialization returned null");
var conflictingFlags = App.FastFlags.Prop.Where(x => list.ContainsKey(x.Key)).Select(x => x.Key);
bool overwriteConflicting = false;
if (conflictingFlags.Any())
{
var result = Controls.ShowMessageBox(
"Some of the flags you are attempting to import already have set values. Would you like to overwrite their current values with the ones defined in the import?\n" +
"\n" +
"Conflicting flags:\n" +
String.Join(", ", conflictingFlags),
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
overwriteConflicting = result == MessageBoxResult.Yes;
}
foreach (var pair in list)
{
if (App.FastFlags.Prop.ContainsKey(pair.Key) && !overwriteConflicting)
continue;
App.FastFlags.SetValue(pair.Key, pair.Value);
}
ClearSearch();
}
catch (Exception ex)
{
Controls.ShowMessageBox(
"The file you've selected does not appear to be valid JSON. Please double check the file contents and try again.\n" +
"\n" +
"More information:\n" +
$"{ex.Message}",
MessageBoxImage.Error
);
}
}
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (sender is not TextBox textbox)
return;
_searchFilter = textbox.Text;
ReloadList();
}
}
}

View File

@ -67,11 +67,9 @@
<TextBlock Text="Presets" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}">
FastFlags for Direct3D
FastFlag preset for Direct3D
<Hyperlink Foreground="{DynamicResource TextFillColorPrimaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#exclusive-fullscreen">exclusive fullscreen</Hyperlink>
(Alt+Enter) and
<Hyperlink Foreground="{DynamicResource TextFillColorPrimaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#dpi-scaling-fixes">DPI scaling fixes</Hyperlink>
are already enabled by default.
using Alt+Enter is already enabled by default.
</TextBlock>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
@ -168,11 +166,40 @@
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Rendering mode" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Choose what graphics renderer Roblox should use." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Choose what renderer Roblox should use. VR requires Direct3D/Automatic." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding RenderingModes.Keys, Mode=OneTime}" Text="{Binding SelectedRenderingMode, Mode=TwoWay}" />
</ui:CardControl>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<TextBlock FontSize="14" Text="Antialiasing quality" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Forces the amount of MSAA samples that are taken." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ComboBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" ItemsSource="{Binding MSAAModes.Keys, Mode=OneTime}" Text="{Binding SelectedMSAAMode, Mode=TwoWay}" />
</ui:CardControl>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" FontSize="14" Text="Preserve rendering quality with display scaling" />
<TextBlock Grid.Column="1" Margin="4,0,0,0">
<Hyperlink TextDecorations="None" ToolTip="More information on this preset" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#dpi-scaling-fixes">
<ui:SymbolIcon Symbol="QuestionCircle48" Margin="0,1,0,0" />
</Hyperlink>
</TextBlock>
</Grid>
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Roblox reduces your rendering quality, depending on display scaling. This toggle disables that." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardControl.Header>
<ui:ToggleSwitch IsChecked="{Binding FixDisplayScaling, Mode=TwoWay}" />
</ui:CardControl>
<ui:CardControl Margin="0,8,0,0">
<ui:CardControl.Header>
<StackPanel>

View File

@ -14,14 +14,14 @@ namespace Bloxstrap.UI
private readonly System.Windows.Forms.NotifyIcon _notifyIcon;
private MenuContainer? _menuContainer;
private RobloxActivity? _activityWatcher;
private ActivityWatcher? _activityWatcher;
private DiscordRichPresence? _richPresenceHandler;
EventHandler? _alertClickHandler;
public NotifyIconWrapper()
{
App.Logger.WriteLine("[NotifyIconWrapper::NotifyIconWrapper] Initializing notification area icon");
App.Logger.WriteLine("NotifyIconWrapper::NotifyIconWrapper", "Initializing notification area icon");
_notifyIcon = new()
{
@ -42,7 +42,7 @@ namespace Bloxstrap.UI
_richPresenceHandler = richPresenceHandler;
}
public void SetActivityWatcher(RobloxActivity activityWatcher)
public void SetActivityWatcher(ActivityWatcher activityWatcher)
{
if (_activityWatcher is not null)
return;
@ -57,10 +57,10 @@ namespace Bloxstrap.UI
#region Context menu
public void InitializeContextMenu()
{
if (_menuContainer is not null)
if (_menuContainer is not null || _disposing)
return;
App.Logger.WriteLine("[NotifyIconWrapper::InitializeContextMenu] Initializing context menu");
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
_menuContainer = new(_activityWatcher, _richPresenceHandler);
_menuContainer.ShowDialog();
@ -94,15 +94,17 @@ namespace Bloxstrap.UI
{
string id = Guid.NewGuid().ToString()[..8];
App.Logger.WriteLine($"[NotifyIconWrapper::ShowAlert] [{id}] Showing alert for {duration} seconds (clickHandler={clickHandler is not null})");
App.Logger.WriteLine($"[NotifyIconWrapper::ShowAlert] [{id}] {caption}: {message.Replace("\n", "\\n")}");
string LOG_IDENT = $"NotifyIconWrapper::ShowAlert.{id}";
App.Logger.WriteLine(LOG_IDENT, $"Showing alert for {duration} seconds (clickHandler={clickHandler is not null})");
App.Logger.WriteLine(LOG_IDENT, $"{caption}: {message.Replace("\n", "\\n")}");
_notifyIcon.BalloonTipTitle = caption;
_notifyIcon.BalloonTipText = message;
if (_alertClickHandler is not null)
{
App.Logger.WriteLine($"[NotifyIconWrapper::ShowAlert] [{id}] Previous alert still present, erasing click handler");
App.Logger.WriteLine(LOG_IDENT, "Previous alert still present, erasing click handler");
_notifyIcon.BalloonTipClicked -= _alertClickHandler;
}
@ -117,12 +119,12 @@ namespace Bloxstrap.UI
_notifyIcon.BalloonTipClicked -= clickHandler;
App.Logger.WriteLine($"[NotifyIconWrapper::ShowAlert] [{id}] Duration over, erasing current click handler");
App.Logger.WriteLine(LOG_IDENT, "Duration over, erasing current click handler");
if (_alertClickHandler == clickHandler)
_alertClickHandler = null;
else
App.Logger.WriteLine($"[NotifyIconWrapper::ShowAlert] [{id}] Click handler has been overriden by another alert");
App.Logger.WriteLine(LOG_IDENT, "Click handler has been overriden by another alert");
});
}
@ -133,7 +135,7 @@ namespace Bloxstrap.UI
_disposing = true;
App.Logger.WriteLine($"[NotifyIconWrapper::Dispose] Disposing NotifyIcon");
App.Logger.WriteLine("NotifyIconWrapper::Dispose", "Disposing NotifyIcon");
_menuContainer?.Dispatcher.Invoke(_menuContainer.Close);
_notifyIcon?.Dispose();

View File

@ -20,7 +20,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
{
get
{
string playerLocation = Path.Combine(Directories.Versions, App.State.Prop.VersionGuid, "RobloxPlayerBeta.exe");
string playerLocation = Path.Combine(Paths.Versions, App.State.Prop.VersionGuid, "RobloxPlayerBeta.exe");
if (!File.Exists(playerLocation))
return "";

View File

@ -0,0 +1,15 @@
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Bloxstrap.UI.ViewModels.Bootstrapper
{
public class FluentDialogViewModel : BootstrapperDialogViewModel
{
public double FooterOpacity => Environment.OSVersion.Version.Build >= 22000 ? 0.4 : 1;
public FluentDialogViewModel(IBootstrapperDialog dialog) : base(dialog)
{
}
}
}

View File

@ -3,12 +3,14 @@ using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Integrations;
namespace Bloxstrap.UI.ViewModels.ContextMenu
{
internal class LogTracerViewModel : NotifyPropertyChangedViewModel
{
private readonly Window _window;
private readonly RobloxActivity _activityWatcher;
private readonly ActivityWatcher _activityWatcher;
private int _lineNumber = 1;
public ICommand CloseWindowCommand => new RelayCommand(_window.Close);
@ -17,7 +19,7 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
public string LogFilename => Path.GetFileName(_activityWatcher.LogLocation);
public string LogContents { get; private set; } = "";
public LogTracerViewModel(Window window, RobloxActivity activityWatcher)
public LogTracerViewModel(Window window, ActivityWatcher activityWatcher)
{
_window = window;
_activityWatcher = activityWatcher;

View File

@ -1,6 +1,6 @@
using System.Windows;
using System.Windows.Input;
using Bloxstrap.Integrations;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.ContextMenu
@ -8,7 +8,7 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
internal class ServerInformationViewModel : NotifyPropertyChangedViewModel
{
private readonly Window _window;
private readonly RobloxActivity _activityWatcher;
private readonly ActivityWatcher _activityWatcher;
public string InstanceId => _activityWatcher.ActivityJobId;
public string ServerType => $"{_activityWatcher.ActivityServerType} server";
@ -18,7 +18,7 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
public ICommand CopyInstanceIdCommand => new RelayCommand(CopyInstanceId);
public ICommand CloseWindowCommand => new RelayCommand(_window.Close);
public ServerInformationViewModel(Window window, RobloxActivity activityWatcher)
public ServerInformationViewModel(Window window, ActivityWatcher activityWatcher)
{
_window = window;
_activityWatcher = activityWatcher;

View File

@ -1,12 +1,8 @@
using System.Windows;
using Bloxstrap.Exceptions;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Menu
{
public class BehaviourViewModel : NotifyPropertyChangedViewModel
{
private bool _manualChannelEntry = !RobloxDeployment.SelectableChannels.Contains(App.Settings.Prop.Channel);
private string _oldVersionGuid = "";
public BehaviourViewModel()
{
@ -15,21 +11,23 @@ namespace Bloxstrap.UI.ViewModels.Menu
private async Task LoadChannelDeployInfo(string channel)
{
LoadingSpinnerVisibility = Visibility.Visible;
LoadingErrorVisibility = Visibility.Collapsed;
ChannelInfoLoadingText = "Fetching latest deploy info, please wait...";
ChannelDeployInfo = null;
const string LOG_IDENT = "BehaviourViewModel::LoadChannelDeployInfo";
OnPropertyChanged(nameof(LoadingSpinnerVisibility));
OnPropertyChanged(nameof(LoadingErrorVisibility));
ShowLoadingError = false;
OnPropertyChanged(nameof(ShowLoadingError));
ChannelInfoLoadingText = "Fetching latest deploy info, please wait...";
OnPropertyChanged(nameof(ChannelInfoLoadingText));
ChannelDeployInfo = null;
OnPropertyChanged(nameof(ChannelDeployInfo));
try
{
ClientVersion info = await RobloxDeployment.GetInfo(channel, true);
ChannelWarningVisibility = info.IsBehindDefaultChannel ? Visibility.Visible : Visibility.Collapsed;
ShowChannelWarning = info.IsBehindDefaultChannel;
OnPropertyChanged(nameof(ShowChannelWarning));
ChannelDeployInfo = new DeployInfo
{
@ -38,43 +36,35 @@ namespace Bloxstrap.UI.ViewModels.Menu
Timestamp = info.Timestamp?.ToFriendlyString()!
};
OnPropertyChanged(nameof(ChannelWarningVisibility));
OnPropertyChanged(nameof(ChannelDeployInfo));
}
catch (HttpResponseUnsuccessfulException ex)
catch (HttpResponseException ex)
{
LoadingSpinnerVisibility = Visibility.Collapsed;
LoadingErrorVisibility = Visibility.Visible;
ShowLoadingError = true;
OnPropertyChanged(nameof(ShowLoadingError));
ChannelInfoLoadingText = ex.ResponseMessage.StatusCode switch
{
HttpStatusCode.NotFound => "The specified channel name does not exist.",
_ => $"Failed to fetch information! (HTTP {ex.ResponseMessage.StatusCode})",
_ => $"Failed to fetch information! (HTTP {(int)ex.ResponseMessage.StatusCode} - {ex.ResponseMessage.ReasonPhrase})",
};
OnPropertyChanged(nameof(LoadingSpinnerVisibility));
OnPropertyChanged(nameof(LoadingErrorVisibility));
OnPropertyChanged(nameof(ChannelInfoLoadingText));
}
catch (Exception ex)
{
LoadingSpinnerVisibility = Visibility.Collapsed;
LoadingErrorVisibility = Visibility.Visible;
App.Logger.WriteLine(LOG_IDENT, "An exception occurred while fetching channel information");
App.Logger.WriteException(LOG_IDENT, ex);
App.Logger.WriteLine("[BehaviourViewModel::LoadChannelDeployInfo] An exception occurred while fetching channel information");
App.Logger.WriteLine($"[BehaviourViewModel::LoadChannelDeployInfo] {ex}");
ShowLoadingError = true;
OnPropertyChanged(nameof(ShowLoadingError));
ChannelInfoLoadingText = $"Failed to fetch information! ({ex.Message})";
OnPropertyChanged(nameof(LoadingSpinnerVisibility));
OnPropertyChanged(nameof(LoadingErrorVisibility));
OnPropertyChanged(nameof(ChannelInfoLoadingText));
}
}
public Visibility LoadingSpinnerVisibility { get; private set; } = Visibility.Visible;
public Visibility LoadingErrorVisibility { get; private set; } = Visibility.Collapsed;
public Visibility ChannelWarningVisibility { get; private set; } = Visibility.Collapsed;
public bool ShowLoadingError { get; set; } = false;
public bool ShowChannelWarning { get; set; } = false;
public DeployInfo? ChannelDeployInfo { get; private set; } = null;
public string ChannelInfoLoadingText { get; private set; } = null!;
@ -108,24 +98,6 @@ namespace Bloxstrap.UI.ViewModels.Menu
}
}
public bool ManualChannelEntry
{
get => _manualChannelEntry;
set
{
_manualChannelEntry = value;
if (!value)
{
// roblox typically sets channels in all lowercase, so here we find if a case insensitive match exists
string? matchingChannel = Channels.Where(x => x.ToLowerInvariant() == SelectedChannel.ToLowerInvariant()).FirstOrDefault();
SelectedChannel = string.IsNullOrEmpty(matchingChannel) ? RobloxDeployment.DefaultChannel : matchingChannel;
}
OnPropertyChanged(nameof(SelectedChannel));
}
}
// todo - move to enum attributes?
public IReadOnlyDictionary<string, ChannelChangeMode> ChannelChangeModes => new Dictionary<string, ChannelChangeMode>
{
@ -139,5 +111,22 @@ namespace Bloxstrap.UI.ViewModels.Menu
get => ChannelChangeModes.FirstOrDefault(x => x.Value == App.Settings.Prop.ChannelChangeMode).Key;
set => App.Settings.Prop.ChannelChangeMode = ChannelChangeModes[value];
}
public bool ForceRobloxReinstallation
{
get => String.IsNullOrEmpty(App.State.Prop.VersionGuid);
set
{
if (value)
{
_oldVersionGuid = App.State.Prop.VersionGuid;
App.State.Prop.VersionGuid = "";
}
else
{
App.State.Prop.VersionGuid = _oldVersionGuid;
}
}
}
}
}

View File

@ -63,7 +63,17 @@ namespace Bloxstrap.UI.ViewModels.Menu
public string SelectedRenderingMode
{
get => App.FastFlags.GetPresetEnum(RenderingModes, "Rendering.Mode", "True");
set => App.FastFlags.SetPresetEnum("Rendering.Mode", RenderingModes[value], "True");
set
{
App.FastFlags.SetPresetEnum("Rendering.Mode", RenderingModes[value], "True");
App.FastFlags.CheckManualFullscreenPreset();
}
}
public bool FixDisplayScaling
{
get => App.FastFlags.GetPreset("Rendering.DisableScaling") == "True";
set => App.FastFlags.SetPreset("Rendering.DisableScaling", value ? "True" : null);
}
public bool AlternateGraphicsSelectorEnabled
@ -117,6 +127,14 @@ namespace Bloxstrap.UI.ViewModels.Menu
set => App.FastFlags.SetPresetEnum("Rendering.Lighting", LightingModes[value], "True");
}
public IReadOnlyDictionary<string, string?> MSAAModes => FastFlagManager.MSAAModes;
public string SelectedMSAAMode
{
get => MSAAModes.First(x => x.Value == App.FastFlags.GetPreset("Rendering.MSAA")).Key ?? MSAAModes.First().Key;
set => App.FastFlags.SetPreset("Rendering.MSAA", MSAAModes[value]);
}
public bool GuiHidingEnabled
{
get => App.FastFlags.GetPreset("UI.Hide") == "32380007";

View File

@ -31,7 +31,7 @@ namespace Bloxstrap.UI.ViewModels.Menu
private void OpenFolder()
{
Process.Start("explorer.exe", Directories.Base);
Process.Start("explorer.exe", Paths.Base);
}
public string InstallLocation

View File

@ -42,7 +42,7 @@ namespace Bloxstrap.UI.ViewModels.Menu
bool shouldCheckInstallLocation = App.IsFirstRun || App.BaseDirectory != _originalBaseDirectory;
if (shouldCheckInstallLocation)
if (shouldCheckInstallLocation && NavigationVisibility == Visibility.Visible)
{
try
{
@ -75,7 +75,7 @@ namespace Bloxstrap.UI.ViewModels.Menu
$"The folder you've chosen to install {App.ProjectName} to already exists and is NOT empty. It is strongly recommended for {App.ProjectName} to be installed to its own independent folder.\n\n" +
"Changing to the following location is suggested:\n" +
$"{suggestedChange}\n\n" +
"Would you like to change your install location to this?\n" +
"Would you like to change to the suggested location?\n" +
"Selecting 'No' will ignore this warning and continue installation.",
MessageBoxImage.Warning,
MessageBoxButton.YesNoCancel,
@ -96,14 +96,15 @@ namespace Bloxstrap.UI.ViewModels.Menu
((INavigationWindow)_window).Navigate(typeof(PreInstallPage));
NavigationVisibility = Visibility.Collapsed;
ConfirmButtonEnabled = false;
OnPropertyChanged(nameof(NavigationVisibility));
ConfirmButtonEnabled = false;
OnPropertyChanged(nameof(ConfirmButtonEnabled));
Task.Run(async delegate
{
await Task.Delay(3000);
ConfirmButtonEnabled = true;
OnPropertyChanged(nameof(ConfirmButtonEnabled));
});
@ -121,7 +122,7 @@ namespace Bloxstrap.UI.ViewModels.Menu
if (shouldCheckInstallLocation)
{
App.Logger.WriteLine($"[MainWindowViewModel::ConfirmSettings] Changing install location from {_originalBaseDirectory} to {App.BaseDirectory}");
App.Logger.WriteLine("MainWindowViewModel::ConfirmSettings", $"Changing install location from {_originalBaseDirectory} to {App.BaseDirectory}");
Controls.ShowMessageBox(
$"{App.ProjectName} will install to the new location you've set the next time it runs.",
@ -131,7 +132,7 @@ namespace Bloxstrap.UI.ViewModels.Menu
using RegistryKey registryKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}");
registryKey.SetValue("InstallLocation", App.BaseDirectory);
registryKey.SetValue("OldInstallLocation", _originalBaseDirectory);
Directories.Initialize(App.BaseDirectory);
Paths.Initialize(App.BaseDirectory);
}
CloseWindow();

View File

@ -9,16 +9,18 @@ namespace Bloxstrap.UI.ViewModels.Menu
{
public class ModsViewModel : NotifyPropertyChangedViewModel
{
private void OpenModsFolder() => Process.Start("explorer.exe", Directories.Modifications);
private void OpenModsFolder() => Process.Start("explorer.exe", Paths.Modifications);
private string _customFontLocation = Path.Combine(Directories.Modifications, "content\\fonts\\CustomFont.ttf");
private bool _usingCustomFont => File.Exists(_customFontLocation);
private bool _usingCustomFont => App.IsFirstRun && App.CustomFontLocation is not null || !App.IsFirstRun && File.Exists(Paths.CustomFont);
private void ManageCustomFont()
{
if (_usingCustomFont)
{
File.Delete(_customFontLocation);
if (App.IsFirstRun)
App.CustomFontLocation = null;
else
File.Delete(Paths.CustomFont);
}
else
{
@ -30,8 +32,16 @@ namespace Bloxstrap.UI.ViewModels.Menu
if (dialog.ShowDialog() != true)
return;
Directory.CreateDirectory(Path.GetDirectoryName(_customFontLocation)!);
File.Copy(dialog.FileName, _customFontLocation);
if (App.IsFirstRun)
{
App.CustomFontLocation = dialog.FileName;
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
File.Copy(dialog.FileName, Paths.CustomFont);
Filesystem.AssertReadOnly(Paths.CustomFont);
}
}
OnPropertyChanged(nameof(ChooseCustomFontVisibility));

View File

@ -1,93 +0,0 @@
using System.Windows;
namespace Bloxstrap
{
public class Updater
{
public static void CheckInstalledVersion()
{
if (Environment.ProcessPath is null || !File.Exists(Directories.Application) || Environment.ProcessPath == Directories.Application)
return;
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol
bool isAutoUpgrade = Environment.ProcessPath.StartsWith(Path.Combine(Directories.Base, "Updates")) || Environment.ProcessPath.StartsWith(Path.Combine(Directories.LocalAppData, "Temp"));
FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath);
if (MD5Hash.FromFile(Environment.ProcessPath) == MD5Hash.FromFile(Directories.Application))
return;
MessageBoxResult result;
// silently upgrade version if the command line flag is set or if we're launching from an auto update
if (App.IsUpgrade || isAutoUpgrade)
{
result = MessageBoxResult.Yes;
}
else
{
result = Controls.ShowMessageBox(
$"The version of {App.ProjectName} you've launched is different to the version you currently have installed.\nWould you like to upgrade your currently installed version?",
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
}
if (result != MessageBoxResult.Yes)
return;
// yes, this is EXTREMELY hacky, but the updater process that launched the
// new version may still be open and so we have to wait for it to close
int attempts = 0;
while (attempts < 10)
{
attempts++;
try
{
File.Delete(Directories.Application);
break;
}
catch (Exception)
{
if (attempts == 1)
App.Logger.WriteLine("[Updater::CheckInstalledVersion] Waiting for write permissions to update version");
Thread.Sleep(500);
}
}
if (attempts == 10)
{
App.Logger.WriteLine("[Updater::CheckInstalledVersion] Failed to update! (Could not get write permissions after 5 seconds)");
return;
}
File.Copy(Environment.ProcessPath, Directories.Application);
Bootstrapper.Register();
if (isAutoUpgrade)
{
App.NotifyIcon?.ShowAlert(
$"Bloxstrap has been upgraded to v{currentVersionInfo.ProductVersion}",
"See what's new in this version",
30,
(_, _) => Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/releases/tag/v{currentVersionInfo.ProductVersion}")
);
}
else if (!App.IsQuiet)
{
Controls.ShowMessageBox(
$"{App.ProjectName} has been updated to v{currentVersionInfo.ProductVersion}",
MessageBoxImage.Information,
MessageBoxButton.OK
);
Controls.ShowMenu();
App.Terminate();
}
}
}
}

View File

@ -1,20 +1,34 @@
namespace Bloxstrap
using System.ComponentModel;
namespace Bloxstrap
{
static class Utilities
{
public static long GetFreeDiskSpace(string path)
public static void ShellExecute(string website)
{
foreach (DriveInfo drive in DriveInfo.GetDrives())
try
{
if (path.StartsWith(drive.Name))
return drive.AvailableFreeSpace;
Process.Start(new ProcessStartInfo
{
FileName = website,
UseShellExecute = true
});
}
catch (Win32Exception ex)
{
// lmfao
return -1;
if (!ex.Message.Contains("Application not found"))
throw;
Process.Start(new ProcessStartInfo
{
FileName = "rundll32.exe",
Arguments = $"shell32,OpenAs_RunDLL {website}"
});
}
}
public static void ShellExecute(string website) => Process.Start(new ProcessStartInfo { FileName = website, UseShellExecute = true });
/// <summary>
///
/// </summary>

View File

@ -0,0 +1,20 @@
namespace Bloxstrap.Utility
{
public static class AsyncHelpers
{
public static void ExceptionHandler(Task task, object? state)
{
const string LOG_IDENT = "AsyncHelpers::ExceptionHandler";
if (task.Exception is null)
return;
if (state is null)
App.Logger.WriteLine(LOG_IDENT, "An exception occurred while running the task");
else
App.Logger.WriteLine(LOG_IDENT, $"An exception occurred while running the task '{state}'");
App.FinalizeExceptionHandling(task.Exception);
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Bloxstrap.Utility
{
internal static class Filesystem
{
internal static long GetFreeDiskSpace(string path)
{
foreach (DriveInfo drive in DriveInfo.GetDrives())
{
if (path.StartsWith(drive.Name))
return drive.AvailableFreeSpace;
}
return -1;
}
internal static void AssertReadOnly(string filePath)
{
var fileInfo = new FileInfo(filePath);
if (!fileInfo.IsReadOnly)
return;
fileInfo.IsReadOnly = false;
App.Logger.WriteLine("Filesystem::AssertReadOnly", $"The following file was set as read-only: {filePath}");
}
}
}

View File

@ -4,6 +4,8 @@
{
public static async Task<T?> GetJson<T>(string url)
{
string LOG_IDENT = $"Http::GetJson<{typeof(T).Name}>";
string json = await App.HttpClient.GetStringAsync(url);
try
@ -12,8 +14,8 @@
}
catch (Exception ex)
{
App.Logger.WriteLine($"[Http::GetJson<{typeof(T).Name}>] Failed to deserialize JSON for {url}!");
App.Logger.WriteLine($"[Http::GetJson<{typeof(T).Name}>] {ex}");
App.Logger.WriteLine(LOG_IDENT, $"Failed to deserialize JSON for {url}!");
App.Logger.WriteException(LOG_IDENT, ex);
return default;
}
}

View File

@ -1,25 +0,0 @@
using System.Runtime.InteropServices;
namespace Bloxstrap.Utility
{
static class NativeMethods
{
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool FlashWindow(IntPtr hWnd, bool bInvert);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
// i only bothered to add the constants that im using lol
public const int GWL_EXSTYLE = -20;
public const int WS_EX_TOOLWINDOW = 0x00000080;
}
}

2
wpfui

@ -1 +1 @@
Subproject commit 5f0a87d7d8bc19335ad1c15a93e1c17efc3c2f6e
Subproject commit 55d5ca08f9a1d7623f9a7e386e1f4ac9f2d024a7