Merge branch 'main' into feature/taskbar-progressbar

This commit is contained in:
bluepilledgreat 2024-09-22 12:40:31 +01:00
commit 89fef194a0
121 changed files with 3880 additions and 2456 deletions

View File

@ -8,6 +8,7 @@ body:
### **Preliminary instructions** ### **Preliminary instructions**
- Before opening an issue, please [check the Wiki first](https://github.com/pizzaboxer/bloxstrap/wiki/) to see if your problem has been addressed there. - Before opening an issue, please [check the Wiki first](https://github.com/pizzaboxer/bloxstrap/wiki/) to see if your problem has been addressed there.
- If it isn't, please confirm which pages that you read that were relevant to your issue. - If it isn't, please confirm which pages that you read that were relevant to your issue.
- Your issue ***will*** be closed without warning if there's a Wiki page addressing your problem.
- If your problem is with Roblox itself (i.e. it crashes or doesn't launch), [check to see if it happens without Bloxstrap](https://github.com/pizzaboxer/bloxstrap/wiki/Roblox-crashes-or-does-not-launch). - If your problem is with Roblox itself (i.e. it crashes or doesn't launch), [check to see if it happens without Bloxstrap](https://github.com/pizzaboxer/bloxstrap/wiki/Roblox-crashes-or-does-not-launch).
- Please only open an issue if your problem happens only with Bloxstrap, and state clearly that this is the case, as anything else is out of my control. - Please only open an issue if your problem happens only with Bloxstrap, and state clearly that this is the case, as anything else is out of my control.
- If you are getting a Bloxstrap Exception error, please attach a copy of the provided log file. There is a button on the dialog that locates it for you. - If you are getting a Bloxstrap Exception error, please attach a copy of the provided log file. There is a button on the dialog that locates it for you.
@ -32,3 +33,10 @@ body:
description: Provide a comprehensive description of the problem you're facing. Don't forget to attach any additional resources you may have, such as log files and screenshots. description: Provide a comprehensive description of the problem you're facing. Don't forget to attach any additional resources you may have, such as log files and screenshots.
validations: validations:
required: true required: true
- type: textarea
id: log
attributes:
label: Bloxstrap Log
description: If you're getting a Bloxstrap Exception error, upload your log file here. Otherwise, just leave it empty.
value: "N/A"
#render: text

View File

@ -15,7 +15,11 @@ namespace Bloxstrap
public partial class App : Application public partial class App : Application
{ {
public const string ProjectName = "Bloxstrap"; public const string ProjectName = "Bloxstrap";
public const string ProjectOwner = "pizzaboxer";
public const string ProjectRepository = "pizzaboxer/bloxstrap"; public const string ProjectRepository = "pizzaboxer/bloxstrap";
public const string ProjectDownloadLink = "https://bloxstraplabs.com";
public const string ProjectHelpLink = "https://github.com/pizzaboxer/bloxstrap/wiki";
public const string ProjectSupportLink = "https://github.com/pizzaboxer/bloxstrap/issues/new";
public const string RobloxPlayerAppName = "RobloxPlayerBeta"; public const string RobloxPlayerAppName = "RobloxPlayerBeta";
public const string RobloxStudioAppName = "RobloxStudioBeta"; public const string RobloxStudioAppName = "RobloxStudioBeta";
@ -29,9 +33,11 @@ namespace Bloxstrap
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static readonly MD5 MD5Provider = MD5.Create(); public static bool IsActionBuild => !String.IsNullOrEmpty(BuildMetadata.CommitRef);
public static NotifyIconWrapper? NotifyIcon { get; set; } public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
public static readonly MD5 MD5Provider = MD5.Create();
public static readonly Logger Logger = new(); public static readonly Logger Logger = new();
@ -49,9 +55,7 @@ namespace Bloxstrap
) )
); );
#if RELEASE
private static bool _showingExceptionDialog = false; private static bool _showingExceptionDialog = false;
#endif
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{ {
@ -59,8 +63,6 @@ namespace Bloxstrap
Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
NotifyIcon?.Dispose();
Environment.Exit(exitCodeNum); Environment.Exit(exitCodeNum);
} }
@ -73,24 +75,51 @@ namespace Bloxstrap
FinalizeExceptionHandling(e.Exception); FinalizeExceptionHandling(e.Exception);
} }
public static void FinalizeExceptionHandling(Exception exception, bool log = true) public static void FinalizeExceptionHandling(AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
Logger.WriteException("App::FinalizeExceptionHandling", innerEx);
FinalizeExceptionHandling(ex.GetBaseException(), false);
}
public static void FinalizeExceptionHandling(Exception ex, bool log = true)
{ {
if (log) if (log)
Logger.WriteException("App::FinalizeExceptionHandling", exception); Logger.WriteException("App::FinalizeExceptionHandling", ex);
#if DEBUG
throw exception;
#else
if (_showingExceptionDialog) if (_showingExceptionDialog)
return; return;
_showingExceptionDialog = true; _showingExceptionDialog = true;
if (!LaunchSettings.QuietFlag.Active) Frontend.ShowExceptionDialog(ex);
Frontend.ShowExceptionDialog(exception);
Terminate(ErrorCode.ERROR_INSTALL_FAILURE); Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
#endif }
public static async Task<GithubRelease?> GetLatestRelease()
{
const string LOG_IDENT = "App::GetLatestRelease";
try
{
var releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest");
if (releaseInfo is null || releaseInfo.Assets is null)
{
Logger.WriteLine(LOG_IDENT, "Encountered invalid data");
return null;
}
return releaseInfo;
}
catch (Exception ex)
{
Logger.WriteException(LOG_IDENT, ex);
}
return null;
} }
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
@ -103,10 +132,10 @@ namespace Bloxstrap
Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}"); Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}");
if (String.IsNullOrEmpty(BuildMetadata.CommitHash)) if (IsActionBuild)
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
else
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})"); Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})");
else
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}"); Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
@ -162,14 +191,7 @@ namespace Bloxstrap
} }
} }
if (installLocation is null) if (fixInstallLocation && installLocation is not null)
{
Logger.Initialize(true);
LaunchHandler.LaunchInstaller();
}
else
{
if (fixInstallLocation)
{ {
var installer = new Installer var installer = new Installer
{ {
@ -182,8 +204,20 @@ namespace Bloxstrap
Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'"); Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'");
installer.DoInstall(); installer.DoInstall();
} }
else
{
// force reinstall
installLocation = null;
}
} }
if (installLocation is null)
{
Logger.Initialize(true);
LaunchHandler.LaunchInstaller();
}
else
{
Paths.Initialize(installLocation); Paths.Initialize(installLocation);
// ensure executable is in the install directory // ensure executable is in the install directory
@ -202,10 +236,6 @@ namespace Bloxstrap
State.Load(); State.Load();
FastFlags.Load(); FastFlags.Load();
// we can only parse them now as settings need
// to be loaded first to know what our channel is
// LaunchSettings.ParseRoblox();
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale)) if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
{ {
Settings.Prop.Locale = "nil"; Settings.Prop.Locale = "nil";
@ -214,13 +244,13 @@ namespace Bloxstrap
Locale.Set(Settings.Prop.Locale); Locale.Set(Settings.Prop.Locale);
if (!LaunchSettings.UninstallFlag.Active) if (!LaunchSettings.BypassUpdateCheck)
Installer.HandleUpgrade(); Installer.HandleUpgrade();
LaunchHandler.ProcessLaunchArgs(); LaunchHandler.ProcessLaunchArgs();
} }
Terminate(); // you must *explicitly* call terminate when everything is done, it won't be called implicitly
} }
} }
} }

View File

@ -39,8 +39,17 @@ namespace Bloxstrap.AppData
{ "extracontent-places.zip", @"ExtraContent\places\" }, { "extracontent-places.zip", @"ExtraContent\places\" },
}; };
public virtual string ExecutableName { get; } = null!;
public virtual string Directory { get; } = null!;
public string LockFilePath => Path.Combine(Directory, "Bloxstrap.lock");
public string ExecutablePath => Path.Combine(Directory, ExecutableName);
public virtual IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } public virtual IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
public CommonAppData() public CommonAppData()
{ {
if (PackageDirectoryMap is null) if (PackageDirectoryMap is null)

View File

@ -1,10 +1,4 @@
using System; namespace Bloxstrap.AppData
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.AppData
{ {
internal interface IAppData internal interface IAppData
{ {
@ -18,6 +12,14 @@ namespace Bloxstrap.AppData
string StartEvent { get; } string StartEvent { get; }
string Directory { get; }
string LockFilePath { get; }
string ExecutablePath { get; }
AppState State { get; }
IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
} }
} }

View File

@ -8,15 +8,19 @@ namespace Bloxstrap.AppData
{ {
public class RobloxPlayerData : CommonAppData, IAppData public class RobloxPlayerData : CommonAppData, IAppData
{ {
public string ProductName { get; } = "Roblox"; public string ProductName => "Roblox";
public string BinaryType { get; } = "WindowsPlayer"; public string BinaryType => "WindowsPlayer";
public string RegistryName { get; } = "RobloxPlayer"; public string RegistryName => "RobloxPlayer";
public string ExecutableName { get; } = "RobloxPlayerBeta.exe"; public override string ExecutableName => "RobloxPlayerBeta.exe";
public string StartEvent { get; } = "www.roblox.com/robloxStartedEvent"; public string StartEvent => "www.roblox.com/robloxStartedEvent";
public override string Directory => Path.Combine(Paths.Roblox, "Player");
public AppState State => App.State.Prop.Player;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>() public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{ {

View File

@ -1,22 +1,20 @@
using System; namespace Bloxstrap.AppData
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.AppData
{ {
public class RobloxStudioData : CommonAppData, IAppData public class RobloxStudioData : CommonAppData, IAppData
{ {
public string ProductName { get; } = "Roblox Studio"; public string ProductName => "Roblox Studio";
public string BinaryType { get; } = "WindowsStudio64"; public string BinaryType => "WindowsStudio64";
public string RegistryName { get; } = "RobloxStudio"; public string RegistryName => "RobloxStudio";
public string ExecutableName { get; } = "RobloxStudioBeta.exe"; public override string ExecutableName => "RobloxStudioBeta.exe";
public string StartEvent { get; } = "www.roblox.com/robloxStudioStartedEvent"; public string StartEvent => "www.roblox.com/robloxStudioStartedEvent";
public override string Directory => Path.Combine(Paths.Roblox, "Studio");
public AppState State => App.State.Prop.Studio;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>() public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{ {

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,7 @@
{ {
[EnumName(FromTranslation = "Common.Automatic")] [EnumName(FromTranslation = "Common.Automatic")]
Default, Default,
// Vulkan,
D3D11, D3D11,
D3D10, D3D10,
// OpenGL
} }
} }

View File

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

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class InvalidHTTPResponseException : Exception
{
public InvalidHTTPResponseException(string message) : base(message) { }
}
}

View File

@ -8,11 +8,28 @@ namespace Bloxstrap.Extensions
{ {
public static Icon GetSized(this Icon icon, int width, int height) => new(icon, new Size(width, height)); public static Icon GetSized(this Icon icon, int width, int height) => new(icon, new Size(width, height));
public static ImageSource GetImageSource(this Icon icon) public static ImageSource GetImageSource(this Icon icon, bool handleException = true)
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
icon.Save(stream); icon.Save(stream);
if (handleException)
{
try
{
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
catch (Exception ex)
{
App.Logger.WriteException("IconEx::GetImageSource", ex);
Frontend.ShowMessageBox(String.Format(Strings.Dialog_IconLoadFailed, ex.Message));
return BootstrapperIcon.IconBloxstrap.GetIcon().GetImageSource(false);
}
}
else
{
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
} }
} }
} }
}

View File

@ -4,9 +4,9 @@
{ {
public static string ToTranslatedString(this ServerType value) => value switch public static string ToTranslatedString(this ServerType value) => value switch
{ {
ServerType.Public => Resources.Strings.Enums_ServerType_Public, ServerType.Public => Strings.Enums_ServerType_Public,
ServerType.Private => Resources.Strings.Enums_ServerType_Private, ServerType.Private => Strings.Enums_ServerType_Private,
ServerType.Reserved => Resources.Strings.Enums_ServerType_Reserved, ServerType.Reserved => Strings.Enums_ServerType_Reserved,
_ => "?" _ => "?"
}; };
} }

View File

@ -9,15 +9,10 @@ namespace Bloxstrap.Extensions
if (dialogTheme != Theme.Default) if (dialogTheme != Theme.Default)
return dialogTheme; return dialogTheme;
RegistryKey? key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
if (key is not null) if (key?.GetValue("AppsUseLightTheme") is int value && value == 0)
{
var value = key.GetValue("AppsUseLightTheme");
if (value is not null && (int)value == 0)
return Theme.Dark; return Theme.Dark;
}
return Theme.Light; return Theme.Light;
} }

View File

@ -4,8 +4,14 @@ namespace Bloxstrap
{ {
public class FastFlagManager : JsonManager<Dictionary<string, object>> public class FastFlagManager : JsonManager<Dictionary<string, object>>
{ {
public override string ClassName => nameof(FastFlagManager);
public override string LOG_IDENT_CLASS => ClassName;
public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json"); public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json");
public bool Changed => !OriginalProp.SequenceEqual(Prop);
public static IReadOnlyDictionary<string, string> PresetFlags = new Dictionary<string, string> public static IReadOnlyDictionary<string, string> PresetFlags = new Dictionary<string, string>
{ {
{ "Network.Log", "FLogNetwork" }, { "Network.Log", "FLogNetwork" },
@ -28,9 +34,6 @@ namespace Bloxstrap
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" }, { "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" }, { "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
{ "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" },
{ "Rendering.Mode.Vulkan.Fix", "FFlagRenderVulkanFixMinimizeWindow" },
{ "Rendering.Mode.OpenGL", "FFlagDebugGraphicsPreferOpenGL" },
{ "Rendering.Lighting.Voxel", "DFFlagDebugRenderForceTechnologyVoxel" }, { "Rendering.Lighting.Voxel", "DFFlagDebugRenderForceTechnologyVoxel" },
{ "Rendering.Lighting.ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" }, { "Rendering.Lighting.ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" },
@ -46,10 +49,9 @@ namespace Bloxstrap
{ "UI.FlagState", "FStringDebugShowFlagState" }, { "UI.FlagState", "FStringDebugShowFlagState" },
#endif #endif
{ "UI.Menu.GraphicsSlider", "FFlagFixGraphicsQuality" },
{ "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" }, { "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" },
{ "UI.Menu.Style.DisableV2", "FFlagDisableNewIGMinDUA" }, { "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" },
{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" }, { "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" },
{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" }, { "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" },
{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" }, { "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" },
@ -59,14 +61,11 @@ namespace Bloxstrap
{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" } { "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" }
}; };
// only one missing here is Metal because lol
public static IReadOnlyDictionary<RenderingMode, string> RenderingModes => new Dictionary<RenderingMode, string> public static IReadOnlyDictionary<RenderingMode, string> RenderingModes => new Dictionary<RenderingMode, string>
{ {
{ RenderingMode.Default, "None" }, { RenderingMode.Default, "None" },
// { RenderingMode.Vulkan, "Vulkan" },
{ RenderingMode.D3D11, "D3D11" }, { RenderingMode.D3D11, "D3D11" },
{ RenderingMode.D3D10, "D3D10" }, { RenderingMode.D3D10, "D3D10" },
// { RenderingMode.OpenGL, "OpenGL" }
}; };
public static IReadOnlyDictionary<LightingMode, string> LightingModes => new Dictionary<LightingMode, string> public static IReadOnlyDictionary<LightingMode, string> LightingModes => new Dictionary<LightingMode, string>
@ -102,7 +101,7 @@ namespace Bloxstrap
InGameMenuVersion.Default, InGameMenuVersion.Default,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", null }, { "V2Rollout", null },
{ "EnableV4", null }, { "EnableV4", null },
{ "EnableV4Chrome", null }, { "EnableV4Chrome", null },
{ "ABTest", null } { "ABTest", null }
@ -113,7 +112,7 @@ namespace Bloxstrap
InGameMenuVersion.V1, InGameMenuVersion.V1,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "True" }, { "V2Rollout", "0" },
{ "EnableV4", "False" }, { "EnableV4", "False" },
{ "EnableV4Chrome", "False" }, { "EnableV4Chrome", "False" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -124,7 +123,7 @@ namespace Bloxstrap
InGameMenuVersion.V2, InGameMenuVersion.V2,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "False" }, { "V2Rollout", "100" },
{ "EnableV4", "False" }, { "EnableV4", "False" },
{ "EnableV4Chrome", "False" }, { "EnableV4Chrome", "False" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -135,7 +134,7 @@ namespace Bloxstrap
InGameMenuVersion.V4, InGameMenuVersion.V4,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "True" }, { "V2Rollout", "0" },
{ "EnableV4", "True" }, { "EnableV4", "True" },
{ "EnableV4Chrome", "False" }, { "EnableV4Chrome", "False" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -146,7 +145,7 @@ namespace Bloxstrap
InGameMenuVersion.V4Chrome, InGameMenuVersion.V4Chrome,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "True" }, { "V2Rollout", "0" },
{ "EnableV4", "True" }, { "EnableV4", "True" },
{ "EnableV4Chrome", "True" }, { "EnableV4Chrome", "True" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -228,14 +227,6 @@ namespace Bloxstrap
return mapping.First().Key; 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() public override void Save()
{ {
// convert all flag values to strings before saving // convert all flag values to strings before saving
@ -244,21 +235,21 @@ namespace Bloxstrap
Prop[pair.Key] = pair.Value.ToString()!; Prop[pair.Key] = pair.Value.ToString()!;
base.Save(); base.Save();
// clone the dictionary
OriginalProp = new(Prop);
} }
public override void Load() public override void Load(bool alertFailure = true)
{ {
base.Load(); base.Load(alertFailure);
CheckManualFullscreenPreset(); // clone the dictionary
OriginalProp = new(Prop);
// TODO - remove when activity tracking has been revamped // TODO - remove when activity tracking has been revamped
if (GetPreset("Network.Log") != "7") if (GetPreset("Network.Log") != "7")
SetPreset("Network.Log", "7"); SetPreset("Network.Log", "7");
string? val = GetPreset("UI.Menu.Style.EnableV4.1");
if (GetPreset("UI.Menu.Style.EnableV4.2") != val)
SetPreset("UI.Menu.Style.EnableV4.2", val);
} }
} }
} }

7
Bloxstrap/GlobalCache.cs Normal file
View File

@ -0,0 +1,7 @@
namespace Bloxstrap
{
public static class GlobalCache
{
public static readonly Dictionary<string, string?> ServerLocation = new();
}
}

View File

@ -3,7 +3,6 @@ global using System.Collections.Generic;
global using System.Diagnostics; global using System.Diagnostics;
global using System.Globalization; global using System.Globalization;
global using System.IO; global using System.IO;
global using System.IO.Compression;
global using System.Text; global using System.Text;
global using System.Text.Json; global using System.Text.Json;
global using System.Text.Json.Serialization; global using System.Text.Json.Serialization;
@ -18,10 +17,16 @@ global using Bloxstrap.Enums;
global using Bloxstrap.Exceptions; global using Bloxstrap.Exceptions;
global using Bloxstrap.Extensions; global using Bloxstrap.Extensions;
global using Bloxstrap.Models; global using Bloxstrap.Models;
global using Bloxstrap.Models.APIs.Config;
global using Bloxstrap.Models.APIs.GitHub;
global using Bloxstrap.Models.APIs.Roblox;
global using Bloxstrap.Models.Attributes; global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.BloxstrapRPC; global using Bloxstrap.Models.BloxstrapRPC;
global using Bloxstrap.Models.RobloxApi; global using Bloxstrap.Models.Entities;
global using Bloxstrap.Models.Manifest; global using Bloxstrap.Models.Manifest;
global using Bloxstrap.Models.Persistable;
global using Bloxstrap.Models.SettingTasks;
global using Bloxstrap.Models.SettingTasks.Base;
global using Bloxstrap.Resources; global using Bloxstrap.Resources;
global using Bloxstrap.UI; global using Bloxstrap.UI;
global using Bloxstrap.Utility; global using Bloxstrap.Utility;

View File

@ -1,9 +1,4 @@
using System.DirectoryServices; using System.Windows;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Windows;
using System.Windows.Media.Animation;
using Bloxstrap.Resources;
using Microsoft.Win32; using Microsoft.Win32;
namespace Bloxstrap namespace Bloxstrap
@ -16,6 +11,8 @@ namespace Bloxstrap
public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap"); public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap");
public bool ExistingDataPresent => File.Exists(Path.Combine(InstallLocation, "Settings.json"));
public bool CreateDesktopShortcuts = true; public bool CreateDesktopShortcuts = true;
public bool CreateStartMenuShortcuts = true; public bool CreateStartMenuShortcuts = true;
@ -26,6 +23,10 @@ namespace Bloxstrap
public void DoInstall() public void DoInstall()
{ {
const string LOG_IDENT = "Installer::DoInstall";
App.Logger.WriteLine(LOG_IDENT, "Beginning installation");
// should've been created earlier from the write test anyway // should've been created earlier from the write test anyway
Directory.CreateDirectory(InstallLocation); Directory.CreateDirectory(InstallLocation);
@ -34,8 +35,20 @@ namespace Bloxstrap
if (!IsImplicitInstall) if (!IsImplicitInstall)
{ {
Filesystem.AssertReadOnly(Paths.Application); Filesystem.AssertReadOnly(Paths.Application);
try
{
File.Copy(Paths.Process, Paths.Application, true); File.Copy(Paths.Process, Paths.Application, true);
} }
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Could not overwrite executable");
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(Strings.Installer_Install_CannotOverwrite, MessageBoxImage.Error);
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
}
}
// TODO: registry access checks, i'll need to look back on issues to see what the error looks like // TODO: registry access checks, i'll need to look back on issues to see what the error looks like
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
@ -50,21 +63,19 @@ namespace Bloxstrap
uninstallKey.SetValue("InstallLocation", Paths.Base); uninstallKey.SetValue("InstallLocation", Paths.Base);
uninstallKey.SetValue("NoRepair", 1); uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", "pizzaboxer"); uninstallKey.SetValue("Publisher", App.ProjectOwner);
uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings"); uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings");
uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall"); uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
} }
// only register player, for the scenario where the user installs bloxstrap, closes it, // only register player, for the scenario where the user installs bloxstrap, closes it,
// and then launches from the website expecting it to work // and then launches from the website expecting it to work
// studio can be implicitly registered when it's first launched manually // studio can be implicitly registered when it's first launched manually
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); WindowsRegistry.RegisterPlayer();
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
// TODO: implicit installation needs to reregister studio
if (CreateDesktopShortcuts) if (CreateDesktopShortcuts)
Shortcut.Create(Paths.Application, "", DesktopShortcut); Shortcut.Create(Paths.Application, "", DesktopShortcut);
@ -73,9 +84,14 @@ namespace Bloxstrap
Shortcut.Create(Paths.Application, "", StartMenuShortcut); Shortcut.Create(Paths.Application, "", StartMenuShortcut);
// existing configuration persisting from an earlier install // existing configuration persisting from an earlier install
App.Settings.Load(); App.Settings.Load(false);
App.State.Load(); App.State.Load(false);
App.FastFlags.Load(); App.FastFlags.Load(false);
if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid))
WindowsRegistry.RegisterStudio();
App.Logger.WriteLine(LOG_IDENT, "Installation finished");
} }
private bool ValidateLocation() private bool ValidateLocation()
@ -88,6 +104,10 @@ namespace Bloxstrap
if (InstallLocation.StartsWith("\\\\")) if (InstallLocation.StartsWith("\\\\"))
return false; return false;
if (InstallLocation.StartsWith(Path.GetTempPath(), StringComparison.InvariantCultureIgnoreCase)
|| InstallLocation.Contains("\\Temp\\", StringComparison.InvariantCultureIgnoreCase))
return false;
// prevent from installing to a onedrive folder // prevent from installing to a onedrive folder
if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase)) if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase))
return false; return false;
@ -158,11 +178,12 @@ namespace Bloxstrap
const string LOG_IDENT = "Installer::DoUninstall"; const string LOG_IDENT = "Installer::DoUninstall";
var processes = new List<Process>(); var processes = new List<Process>();
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName)); processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
#if STUDIO_FEATURES if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid))
processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName)); processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName));
#endif
// prompt to shutdown roblox if its currently running // prompt to shutdown roblox if its currently running
if (processes.Any()) if (processes.Any())
@ -175,7 +196,10 @@ namespace Bloxstrap
); );
if (result != MessageBoxResult.OK) if (result != MessageBoxResult.OK)
{
App.Terminate(ErrorCode.ERROR_CANCELLED); App.Terminate(ErrorCode.ERROR_CANCELLED);
return;
}
try try
{ {
@ -203,44 +227,38 @@ namespace Bloxstrap
{ {
playerStillInstalled = false; playerStillInstalled = false;
ProtocolHandler.Unregister("roblox"); WindowsRegistry.Unregister("roblox");
ProtocolHandler.Unregister("roblox-player"); WindowsRegistry.Unregister("roblox-player");
} }
else else
{ {
// revert launch uri handler to stock bootstrapper
string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe"); string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe");
ProtocolHandler.Register("roblox", "Roblox", playerPath); WindowsRegistry.RegisterPlayer(playerPath, "%1");
ProtocolHandler.Register("roblox-player", "Roblox", playerPath);
} }
using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); using var studioKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
if (studioBootstrapperKey is null) var studioFolder = studioKey?.GetValue("InstallLocation");
if (studioKey is null || studioFolder is not string)
{ {
studioStillInstalled = false; studioStillInstalled = false;
#if STUDIO_FEATURES WindowsRegistry.Unregister("roblox-studio");
ProtocolHandler.Unregister("roblox-studio"); WindowsRegistry.Unregister("roblox-studio-auth");
ProtocolHandler.Unregister("roblox-studio-auth");
ProtocolHandler.Unregister("Roblox.Place"); WindowsRegistry.Unregister("Roblox.Place");
ProtocolHandler.Unregister(".rbxl"); WindowsRegistry.Unregister(".rbxl");
ProtocolHandler.Unregister(".rbxlx"); WindowsRegistry.Unregister(".rbxlx");
#endif
} }
#if STUDIO_FEATURES
else else
{ {
string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper string studioPath = Path.Combine((string)studioFolder, "RobloxStudioBeta.exe");
ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation); string studioLauncherPath = Path.Combine((string)studioFolder, "RobloxStudioLauncherBeta.exe");
ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation);
ProtocolHandler.RegisterRobloxPlace(studioLocation); WindowsRegistry.RegisterStudioProtocol(studioPath, "%1");
WindowsRegistry.RegisterStudioFileClass(studioPath, "-ide \"%1\"");
} }
#endif
var cleanupSequence = new List<Action> var cleanupSequence = new List<Action>
{ {
@ -257,8 +275,10 @@ namespace Bloxstrap
() => File.Delete(StartMenuShortcut), () => File.Delete(StartMenuShortcut),
() => Directory.Delete(Paths.Versions, true),
() => Directory.Delete(Paths.Downloads, true), () => Directory.Delete(Paths.Downloads, true),
() => Directory.Delete(Paths.Roblox, true),
() => File.Delete(App.State.FileLocation)
}; };
if (!keepData) if (!keepData)
@ -268,8 +288,7 @@ namespace Bloxstrap
() => Directory.Delete(Paths.Modifications, true), () => Directory.Delete(Paths.Modifications, true),
() => Directory.Delete(Paths.Logs, true), () => Directory.Delete(Paths.Logs, true),
() => File.Delete(App.Settings.FileLocation), () => File.Delete(App.Settings.FileLocation)
() => File.Delete(App.State.FileLocation), // TODO: maybe this should always be deleted? not sure yet
}); });
} }
@ -331,8 +350,9 @@ namespace Bloxstrap
return; return;
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol // 2.0.0 downloads updates to <BaseFolder>/Updates so lol
// TODO: 2.8.0 will download them to <Temp>/Bloxstrap/Updates bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active
bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")); || Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates"))
|| Paths.Process.StartsWith(Paths.Temp);
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion; var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion;
@ -340,8 +360,20 @@ namespace Bloxstrap
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application)) if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
return; return;
if (currentVer is not null && existingVer is not null && Utilities.CompareVersions(currentVer, existingVer) == VersionComparison.LessThan)
{
var result = Frontend.ShowMessageBox(
Strings.InstallChecker_VersionLessThanInstalled,
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
if (result != MessageBoxResult.Yes)
return;
}
// silently upgrade version if the command line flag is set or if we're launching from an auto update // silently upgrade version if the command line flag is set or if we're launching from an auto update
if (!App.LaunchSettings.UpgradeFlag.Active && !isAutoUpgrade) if (!isAutoUpgrade)
{ {
var result = Frontend.ShowMessageBox( var result = Frontend.ShowMessageBox(
Strings.InstallChecker_VersionDifferentThanInstalled, Strings.InstallChecker_VersionDifferentThanInstalled,
@ -353,47 +385,94 @@ namespace Bloxstrap
return; return;
} }
App.Logger.WriteLine(LOG_IDENT, "Doing upgrade");
Filesystem.AssertReadOnly(Paths.Application); Filesystem.AssertReadOnly(Paths.Application);
// TODO: make this use a mutex somehow using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5)))
// 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++; if (!ipl.IsAcquired)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)");
return;
}
}
// prior to 2.8.0, auto-updating was handled with this... bruteforce method
// now it's handled with the system mutex you see above, but we need to keep this logic for <2.8.0 versions
for (int i = 1; i <= 10; i++)
{
try try
{ {
File.Delete(Paths.Application); File.Copy(Paths.Process, Paths.Application, true);
break; break;
} }
catch (Exception) catch (Exception ex)
{
if (i == 1)
{ {
if (attempts == 1)
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
}
else if (i == 10)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 10 tries/5 seconds)");
App.Logger.WriteException(LOG_IDENT, ex);
return;
}
Thread.Sleep(500); 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);
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{ {
uninstallKey.SetValue("DisplayVersion", App.Version); uninstallKey.SetValue("DisplayVersion", App.Version);
uninstallKey.SetValue("Publisher", App.ProjectOwner);
uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
} }
// update migrations // update migrations
if (existingVer is not null) if (existingVer is not null)
{ {
if (Utilities.CompareVersions(existingVer, "2.2.0") == VersionComparison.LessThan)
{
string path = Path.Combine(Paths.Integrations, "rbxfpsunlocker");
try
{
if (Directory.Exists(path))
Directory.Delete(path, true);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
}
}
if (Utilities.CompareVersions(existingVer, "2.3.0") == VersionComparison.LessThan)
{
string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll");
string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini");
if (File.Exists(injectorLocation))
{
Frontend.ShowMessageBox(
Strings.Bootstrapper_HyperionUpdateInfo,
MessageBoxImage.Warning
);
File.Delete(injectorLocation);
}
if (File.Exists(configLocation))
File.Delete(configLocation);
}
if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan) if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan)
{ {
App.FastFlags.SetValue("DFFlagDisableDPIScale", null); App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
@ -408,6 +487,13 @@ namespace Bloxstrap
App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false); App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false);
} }
if (Utilities.CompareVersions(existingVer, "2.5.3") == VersionComparison.LessThan)
{
string? val = App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.1");
if (App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.2") != val)
App.FastFlags.SetPreset("UI.Menu.Style.EnableV4.2", val);
}
if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan) if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan)
{ {
if (App.Settings.Prop.UseDisableAppPatch) if (App.Settings.Prop.UseDisableAppPatch)
@ -429,10 +515,8 @@ namespace Bloxstrap
_ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x); _ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x);
if (x == 0) if (x == 0)
{
App.FastFlags.SetPreset("Rendering.Framerate", null); App.FastFlags.SetPreset("Rendering.Framerate", null);
} }
}
if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan) if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan)
{ {
@ -440,7 +524,7 @@ namespace Bloxstrap
string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap"); string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap");
if (File.Exists(oldDesktopPath)) if (File.Exists(oldDesktopPath))
File.Move(oldDesktopPath, DesktopShortcut); File.Move(oldDesktopPath, DesktopShortcut, true);
if (Directory.Exists(oldStartPath)) if (Directory.Exists(oldStartPath))
{ {
@ -458,14 +542,32 @@ namespace Bloxstrap
Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false); Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false);
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); WindowsRegistry.RegisterPlayer();
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
string? oldV2Val = App.FastFlags.GetValue("FFlagDisableNewIGMinDUA");
if (oldV2Val is not null)
{
if (oldV2Val == "True")
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "0");
else
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "100");
App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null);
}
App.FastFlags.SetValue("FFlagFixGraphicsQuality", null);
Directory.Delete(Path.Combine(Paths.Base, "Versions"));
} }
App.Settings.Save(); App.Settings.Save();
App.FastFlags.Save(); App.FastFlags.Save();
} }
if (currentVer is null)
return;
if (isAutoUpgrade) if (isAutoUpgrade)
{ {
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}"); Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}");

View File

@ -2,24 +2,29 @@
{ {
public class ActivityWatcher : IDisposable public class ActivityWatcher : IDisposable
{ {
// i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence, private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
// like checking the ping and region of the current connected server. maybe that's something to add?
private const string GameJoiningEntry = "[FLog::Output] ! Joining game"; private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
// these entries are technically volatile!
// they only get printed depending on their configured FLog level, which could change at any time
// while levels being changed is fairly rare, please limit the number of varying number of FLog types you have to use, if possible
private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer"; private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer"; private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = "; private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
private const string GameJoinedEntry = "[FLog::Network] serverId:"; private const string GameJoinedEntry = "[FLog::Network] serverId:";
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:"; private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport"; private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal"; private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal";
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
private const string GameJoiningPrivateServerPattern = @"""accessCode"":""([0-9a-f\-]{36})""";
private const string GameJoiningUniversePattern = @"universeid:([0-9]+)";
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+"; private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+"; private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)"; private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)";
private int _gameClientPid;
private int _logEntriesRead = 0; private int _logEntriesRead = 0;
private bool _teleportMarker = false; private bool _teleportMarker = false;
private bool _reservedTeleportMarker = false; private bool _reservedTeleportMarker = false;
@ -27,34 +32,28 @@
public event EventHandler<string>? OnLogEntry; public event EventHandler<string>? OnLogEntry;
public event EventHandler? OnGameJoin; public event EventHandler? OnGameJoin;
public event EventHandler? OnGameLeave; public event EventHandler? OnGameLeave;
public event EventHandler? OnLogOpen;
public event EventHandler? OnAppClose; public event EventHandler? OnAppClose;
public event EventHandler<Message>? OnRPCMessage; public event EventHandler<Message>? OnRPCMessage;
private readonly Dictionary<string, string> GeolocationCache = new();
private DateTime LastRPCRequest; private DateTime LastRPCRequest;
public string LogLocation = null!; public string LogLocation = null!;
// these are values to use assuming the player isn't currently in a game public bool InGame = false;
// hmm... do i move this to a model?
public bool ActivityInGame = false; public ActivityData Data { get; private set; } = new();
public long ActivityPlaceId = 0;
public string ActivityJobId = ""; /// <summary>
public string ActivityMachineAddress = ""; /// Ordered by newest to oldest
public bool ActivityMachineUDMUX = false; /// </summary>
public bool ActivityIsTeleport = false; public List<ActivityData> History = new();
public ServerType ActivityServerType = ServerType.Public;
public bool IsDisposed = false; public bool IsDisposed = false;
public ActivityWatcher(int gameClientPid) public async void Start()
{ {
_gameClientPid = gameClientPid; const string LOG_IDENT = "ActivityWatcher::Start";
}
public async void StartWatcher()
{
const string LOG_IDENT = "ActivityWatcher::StartWatcher";
// okay, here's the process: // okay, here's the process:
// //
@ -84,7 +83,7 @@
{ {
logFileInfo = new DirectoryInfo(logDirectory) logFileInfo = new DirectoryInfo(logDirectory)
.GetFiles() .GetFiles()
.Where(x => x.CreationTime <= DateTime.Now) .Where(x => x.Name.Contains("Player", StringComparison.OrdinalIgnoreCase) && x.CreationTime <= DateTime.Now)
.OrderByDescending(x => x.CreationTime) .OrderByDescending(x => x.CreationTime)
.First(); .First();
@ -95,12 +94,14 @@
await Task.Delay(1000); await Task.Delay(1000);
} }
OnLogOpen?.Invoke(this, EventArgs.Empty);
LogLocation = logFileInfo.FullName; LogLocation = logFileInfo.FullName;
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}"); App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");
AutoResetEvent logUpdatedEvent = new(false); var logUpdatedEvent = new AutoResetEvent(false);
FileSystemWatcher logWatcher = new() var logWatcher = new FileSystemWatcher()
{ {
Path = logDirectory, Path = logDirectory,
Filter = Path.GetFileName(logFileInfo.FullName), Filter = Path.GetFileName(logFileInfo.FullName),
@ -108,7 +109,7 @@
}; };
logWatcher.Changed += (s, e) => logUpdatedEvent.Set(); logWatcher.Changed += (s, e) => logUpdatedEvent.Set();
using StreamReader sr = new(logFileStream); using var sr = new StreamReader(logFileStream);
while (!IsDisposed) while (!IsDisposed)
{ {
@ -117,13 +118,13 @@
if (log is null) if (log is null)
logUpdatedEvent.WaitOne(250); logUpdatedEvent.WaitOne(250);
else else
ExamineLogEntry(log); ReadLogEntry(log);
} }
} }
private void ExamineLogEntry(string entry) private void ReadLogEntry(string entry)
{ {
const string LOG_IDENT = "ActivityWatcher::ExamineLogEntry"; const string LOG_IDENT = "ActivityWatcher::ReadLogEntry";
OnLogEntry?.Invoke(this, entry); OnLogEntry?.Invoke(this, entry);
@ -137,14 +138,38 @@
App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries"); App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries");
if (entry.Contains(GameLeavingEntry)) if (entry.Contains(GameLeavingEntry))
OnAppClose?.Invoke(this, new EventArgs());
if (!ActivityInGame && ActivityPlaceId == 0)
{ {
App.Logger.WriteLine(LOG_IDENT, "User is back into the desktop app");
OnAppClose?.Invoke(this, EventArgs.Empty);
if (Data.PlaceId != 0 && !InGame)
{
App.Logger.WriteLine(LOG_IDENT, "User appears to be leaving from a cancelled/errored join");
Data = new();
}
}
if (!InGame && Data.PlaceId == 0)
{
// We are not in a game, nor are in the process of joining one
if (entry.Contains(GameJoiningPrivateServerEntry)) if (entry.Contains(GameJoiningPrivateServerEntry))
{ {
// we only expect to be joining a private server if we're not already in a game // we only expect to be joining a private server if we're not already in a game
ActivityServerType = ServerType.Private;
Data.ServerType = ServerType.Private;
var match = Regex.Match(entry, GameJoiningPrivateServerPattern);
if (match.Groups.Count != 2)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join private server entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
Data.AccessCode = match.Groups[1].Value;
} }
else if (entry.Contains(GameJoiningEntry)) else if (entry.Contains(GameJoiningEntry))
{ {
@ -157,80 +182,111 @@
return; return;
} }
ActivityInGame = false; InGame = false;
ActivityPlaceId = long.Parse(match.Groups[2].Value); Data.PlaceId = long.Parse(match.Groups[2].Value);
ActivityJobId = match.Groups[1].Value; Data.JobId = match.Groups[1].Value;
ActivityMachineAddress = match.Groups[3].Value; Data.MachineAddress = match.Groups[3].Value;
if (App.Settings.Prop.ShowServerDetails && Data.MachineAddressValid)
_ = Data.QueryServerLocation();
if (_teleportMarker) if (_teleportMarker)
{ {
ActivityIsTeleport = true; Data.IsTeleport = true;
_teleportMarker = false; _teleportMarker = false;
} }
if (_reservedTeleportMarker) if (_reservedTeleportMarker)
{ {
ActivityServerType = ServerType.Reserved; Data.ServerType = ServerType.Reserved;
_reservedTeleportMarker = false; _reservedTeleportMarker = false;
} }
App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({Data})");
} }
} }
else if (!ActivityInGame && ActivityPlaceId != 0) else if (!InGame && Data.PlaceId != 0)
{ {
if (entry.Contains(GameJoiningUDMUXEntry)) // We are not confirmed to be in a game, but we are in the process of joining one
{
Match match = Regex.Match(entry, GameJoiningUDMUXPattern);
if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress) if (entry.Contains(GameJoiningUniverseEntry))
{ {
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join UDMUX entry"); var match = Regex.Match(entry, GameJoiningUniversePattern);
if (match.Groups.Count != 2)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join universe entry");
App.Logger.WriteLine(LOG_IDENT, entry); App.Logger.WriteLine(LOG_IDENT, entry);
return; return;
} }
ActivityMachineAddress = match.Groups[1].Value; Data.UniverseId = long.Parse(match.Groups[1].Value);
ActivityMachineUDMUX = true;
App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); if (History.Any())
{
var lastActivity = History.First();
if (Data.UniverseId == lastActivity.UniverseId && Data.IsTeleport)
Data.RootActivity = lastActivity.RootActivity ?? lastActivity;
}
}
else if (entry.Contains(GameJoiningUDMUXEntry))
{
var match = Regex.Match(entry, GameJoiningUDMUXPattern);
if (match.Groups.Count != 3 || match.Groups[2].Value != Data.MachineAddress)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
Data.MachineAddress = match.Groups[1].Value;
if (App.Settings.Prop.ShowServerDetails)
_ = Data.QueryServerLocation();
App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({Data})");
} }
else if (entry.Contains(GameJoinedEntry)) else if (entry.Contains(GameJoinedEntry))
{ {
Match match = Regex.Match(entry, GameJoinedEntryPattern); Match match = Regex.Match(entry, GameJoinedEntryPattern);
if (match.Groups.Count != 2 || match.Groups[1].Value != ActivityMachineAddress) if (match.Groups.Count != 2 || match.Groups[1].Value != Data.MachineAddress)
{ {
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry"); App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry");
App.Logger.WriteLine(LOG_IDENT, entry); App.Logger.WriteLine(LOG_IDENT, entry);
return; return;
} }
App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({Data})");
InGame = true;
Data.TimeJoined = DateTime.Now;
ActivityInGame = true;
OnGameJoin?.Invoke(this, new EventArgs()); OnGameJoin?.Invoke(this, new EventArgs());
} }
} }
else if (ActivityInGame && ActivityPlaceId != 0) else if (InGame && Data.PlaceId != 0)
{ {
// We are confirmed to be in a game
if (entry.Contains(GameDisconnectedEntry)) if (entry.Contains(GameDisconnectedEntry))
{ {
App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({Data})");
ActivityInGame = false; Data.TimeLeft = DateTime.Now;
ActivityPlaceId = 0; History.Insert(0, Data);
ActivityJobId = "";
ActivityMachineAddress = ""; InGame = false;
ActivityMachineUDMUX = false;
ActivityIsTeleport = false; Data = new();
ActivityServerType = ServerType.Public;
OnGameLeave?.Invoke(this, new EventArgs()); OnGameLeave?.Invoke(this, new EventArgs());
} }
else if (entry.Contains(GameTeleportingEntry)) else if (entry.Contains(GameTeleportingEntry))
{ {
App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({Data})");
_teleportMarker = true; _teleportMarker = true;
} }
else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry)) else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry))
@ -282,6 +338,35 @@
return; return;
} }
if (message.Command == "SetLaunchData")
{
string? data;
try
{
data = message.Data.Deserialize<string>();
}
catch (Exception)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
return;
}
if (data is null)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
if (data.Length > 200)
{
App.Logger.WriteLine(LOG_IDENT, "Data cannot be longer than 200 characters");
return;
}
Data.RPCLaunchData = data;
}
OnRPCMessage?.Invoke(this, message); OnRPCMessage?.Invoke(this, message);
LastRPCRequest = DateTime.Now; LastRPCRequest = DateTime.Now;
@ -289,44 +374,6 @@
} }
} }
public async Task<string> GetServerLocation()
{
const string LOG_IDENT = "ActivityWatcher::GetServerLocation";
if (GeolocationCache.ContainsKey(ActivityMachineAddress))
return GeolocationCache[ActivityMachineAddress];
try
{
string location = "";
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{ActivityMachineAddress}/json");
if (ipInfo is null)
return $"? ({Resources.Strings.ActivityTracker_LookupFailed})";
if (string.IsNullOrEmpty(ipInfo.Country))
location = "?";
else if (ipInfo.City == ipInfo.Region)
location = $"{ipInfo.Region}, {ipInfo.Country}";
else
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
if (!ActivityInGame)
return $"? ({Resources.Strings.ActivityTracker_LeftGame})";
GeolocationCache[ActivityMachineAddress] = location;
return location;
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {ActivityMachineAddress}");
App.Logger.WriteException(LOG_IDENT, ex);
return $"? ({Resources.Strings.ActivityTracker_LookupFailed})";
}
}
public void Dispose() public void Dispose()
{ {
IsDisposed = true; IsDisposed = true;

View File

@ -1,4 +1,6 @@
using DiscordRPC; using System.Windows;
using DiscordRPC;
namespace Bloxstrap.Integrations namespace Bloxstrap.Integrations
{ {
@ -6,18 +8,16 @@ namespace Bloxstrap.Integrations
{ {
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486"); private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
private readonly ActivityWatcher _activityWatcher; private readonly ActivityWatcher _activityWatcher;
private readonly Queue<Message> _messageQueue = new();
private DiscordRPC.RichPresence? _currentPresence; private DiscordRPC.RichPresence? _currentPresence;
private DiscordRPC.RichPresence? _currentPresenceCopy; private DiscordRPC.RichPresence? _originalPresence;
private Message? _stashedRPCMessage;
private bool _visible = true; private bool _visible = true;
private long _currentUniverseId;
private DateTime? _timeStartedUniverse;
public DiscordRichPresence(ActivityWatcher activityWatcher) public DiscordRichPresence(ActivityWatcher activityWatcher)
{ {
const string LOG_IDENT = "DiscordRichPresence::DiscordRichPresence"; const string LOG_IDENT = "DiscordRichPresence";
_activityWatcher = activityWatcher; _activityWatcher = activityWatcher;
@ -47,30 +47,30 @@ namespace Bloxstrap.Integrations
_rpcClient.Initialize(); _rpcClient.Initialize();
} }
public void ProcessRPCMessage(Message message) public void ProcessRPCMessage(Message message, bool implicitUpdate = true)
{ {
const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage"; const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage";
if (message.Command != "SetRichPresence") if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData")
return; return;
if (_currentPresence is null || _currentPresenceCopy is null) if (_currentPresence is null || _originalPresence is null)
{ {
if (_activityWatcher.ActivityInGame) App.Logger.WriteLine(LOG_IDENT, "Presence is not set, enqueuing message");
{ _messageQueue.Enqueue(message);
App.Logger.WriteLine(LOG_IDENT, "Presence is not yet set, but is currently in game, stashing presence set request");
_stashedRPCMessage = message;
return; return;
} }
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, aborting");
return;
}
Models.BloxstrapRPC.RichPresence? presenceData;
// a lot of repeated code here, could this somehow be cleaned up? // a lot of repeated code here, could this somehow be cleaned up?
if (message.Command == "SetLaunchData")
{
_currentPresence.Buttons = GetButtons();
}
else if (message.Command == "SetRichPresence")
{
Models.BloxstrapRPC.RichPresence? presenceData;
try try
{ {
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>(); presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
@ -92,7 +92,7 @@ namespace Bloxstrap.Integrations
if (presenceData.Details.Length > 128) if (presenceData.Details.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters"); App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
else if (presenceData.Details == "<reset>") else if (presenceData.Details == "<reset>")
_currentPresence.Details = _currentPresenceCopy.Details; _currentPresence.Details = _originalPresence.Details;
else else
_currentPresence.Details = presenceData.Details; _currentPresence.Details = presenceData.Details;
} }
@ -102,7 +102,7 @@ namespace Bloxstrap.Integrations
if (presenceData.State.Length > 128) if (presenceData.State.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters"); App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
else if (presenceData.State == "<reset>") else if (presenceData.State == "<reset>")
_currentPresence.State = _currentPresenceCopy.State; _currentPresence.State = _originalPresence.State;
else else
_currentPresence.State = presenceData.State; _currentPresence.State = presenceData.State;
} }
@ -125,8 +125,8 @@ namespace Bloxstrap.Integrations
} }
else if (presenceData.SmallImage.Reset) else if (presenceData.SmallImage.Reset)
{ {
_currentPresence.Assets.SmallImageText = _currentPresenceCopy.Assets.SmallImageText; _currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
_currentPresence.Assets.SmallImageKey = _currentPresenceCopy.Assets.SmallImageKey; _currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
} }
else else
{ {
@ -146,8 +146,8 @@ namespace Bloxstrap.Integrations
} }
else if (presenceData.LargeImage.Reset) else if (presenceData.LargeImage.Reset)
{ {
_currentPresence.Assets.LargeImageText = _currentPresenceCopy.Assets.LargeImageText; _currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
_currentPresence.Assets.LargeImageKey = _currentPresenceCopy.Assets.LargeImageKey; _currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
} }
else else
{ {
@ -158,7 +158,9 @@ namespace Bloxstrap.Integrations
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText; _currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
} }
} }
}
if (implicitUpdate)
UpdatePresence(); UpdatePresence();
} }
@ -178,124 +180,131 @@ namespace Bloxstrap.Integrations
{ {
const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame"; const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame";
if (!_activityWatcher.ActivityInGame) if (!_activityWatcher.InGame)
{ {
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence"); App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
_currentPresence = _currentPresenceCopy = null; _currentPresence = _originalPresence = null;
_stashedRPCMessage = null; _messageQueue.Clear();
UpdatePresence(); UpdatePresence();
return true; return true;
} }
string icon = "roblox"; string icon = "roblox";
long placeId = _activityWatcher.ActivityPlaceId;
var activity = _activityWatcher.Data;
long placeId = activity.PlaceId;
App.Logger.WriteLine(LOG_IDENT, $"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(LOG_IDENT, "Could not get Universe ID!");
return false;
}
long universeId = universeIdResponse.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 // preserve time spent playing if we're teleporting between places in the same universe
if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId) var timeStarted = activity.TimeJoined;
_timeStartedUniverse = DateTime.UtcNow;
_currentUniverseId = universeId; if (activity.RootActivity is not null)
timeStarted = activity.RootActivity.TimeJoined;
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeId}"); if (activity.UniverseDetails is null)
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
{ {
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe info!"); try
{
await UniverseDetails.FetchSingle(activity.UniverseId);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox($"{Strings.ActivityWatcher_RichPresenceLoadFailed}\n\n{ex.Message}", MessageBoxImage.Warning);
return false; return false;
} }
GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0]; activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId);
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(LOG_IDENT, "Could not get Universe thumbnail info!");
}
else
{
icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl;
App.Logger.WriteLine(LOG_IDENT, $"Got Universe thumbnail as {icon}");
} }
List<Button> buttons = new(); var universeDetails = activity.UniverseDetails!;
if (!App.Settings.Prop.HideRPCButtons && _activityWatcher.ActivityServerType == ServerType.Public) icon = universeDetails.Thumbnail.ImageUrl;
{
buttons.Add(new Button
{
Label = "Join server",
Url = $"roblox://experiences/start?placeId={placeId}&gameInstanceId={_activityWatcher.ActivityJobId}"
});
}
buttons.Add(new Button if (!_activityWatcher.InGame || placeId != activity.PlaceId)
{
Label = "See game page",
Url = $"https://www.roblox.com/games/{placeId}"
});
if (!_activityWatcher.ActivityInGame || placeId != _activityWatcher.ActivityPlaceId)
{ {
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed"); App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
return false; return false;
} }
string status = _activityWatcher.ActivityServerType switch string status = _activityWatcher.Data.ServerType switch
{ {
ServerType.Private => "In a private server", ServerType.Private => "In a private server",
ServerType.Reserved => "In a reserved server", ServerType.Reserved => "In a reserved server",
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""), _ => $"by {universeDetails.Data.Creator.Name}" + (universeDetails.Data.Creator.HasVerifiedBadge ? " ☑️" : ""),
}; };
if (universeDetails.Name.Length < 2) string universeName = universeDetails.Data.Name;
universeDetails.Name = $"{universeDetails.Name}\x2800\x2800\x2800";
if (universeName.Length < 2)
universeName = $"{universeName}\x2800\x2800\x2800";
_currentPresence = new DiscordRPC.RichPresence _currentPresence = new DiscordRPC.RichPresence
{ {
Details = $"Playing {universeDetails.Name}", Details = universeName,
State = status, State = status,
Timestamps = new Timestamps { Start = _timeStartedUniverse }, Timestamps = new Timestamps { Start = timeStarted.ToUniversalTime() },
Buttons = buttons.ToArray(), Buttons = GetButtons(),
Assets = new Assets Assets = new Assets
{ {
LargeImageKey = icon, LargeImageKey = icon,
LargeImageText = universeDetails.Name, LargeImageText = universeName,
SmallImageKey = "roblox", SmallImageKey = "roblox",
SmallImageText = "Roblox" SmallImageText = "Roblox"
} }
}; };
// this is used for configuration from BloxstrapRPC // this is used for configuration from BloxstrapRPC
_currentPresenceCopy = _currentPresence.Clone(); _originalPresence = _currentPresence.Clone();
if (_stashedRPCMessage is not null) if (_messageQueue.Any())
{ {
App.Logger.WriteLine(LOG_IDENT, "Found stashed RPC message, invoking presence set command now"); App.Logger.WriteLine(LOG_IDENT, "Processing queued messages");
ProcessRPCMessage(_stashedRPCMessage); ProcessRPCMessage(_messageQueue.Dequeue(), false);
_stashedRPCMessage = null;
} }
else
{
UpdatePresence(); UpdatePresence();
}
return true; return true;
} }
public Button[] GetButtons()
{
var buttons = new List<Button>();
var data = _activityWatcher.Data;
if (!App.Settings.Prop.HideRPCButtons)
{
bool show = false;
if (data.ServerType == ServerType.Public)
show = true;
else if (data.ServerType == ServerType.Reserved && !String.IsNullOrEmpty(data.RPCLaunchData))
show = true;
if (show)
{
buttons.Add(new Button
{
Label = "Join server",
Url = data.GetInviteDeeplink()
});
}
}
buttons.Add(new Button
{
Label = "See game page",
Url = $"https://www.roblox.com/games/{data.PlaceId}"
});
return buttons.ToArray();
}
public void UpdatePresence() public void UpdatePresence()
{ {
const string LOG_IDENT = "DiscordRichPresence::UpdatePresence"; const string LOG_IDENT = "DiscordRichPresence::UpdatePresence";

View File

@ -4,13 +4,17 @@ namespace Bloxstrap
{ {
public class JsonManager<T> where T : class, new() public class JsonManager<T> where T : class, new()
{ {
public T OriginalProp { get; set; } = new();
public T Prop { get; set; } = new(); public T Prop { get; set; } = new();
public virtual string FileLocation => Path.Combine(Paths.Base, $"{typeof(T).Name}.json"); public virtual string ClassName => typeof(T).Name;
private string LOG_IDENT_CLASS => $"JsonManager<{typeof(T).Name}>"; public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
public virtual void Load() public virtual string LOG_IDENT_CLASS => $"JsonManager<{ClassName}>";
public virtual void Load(bool alertFailure = true)
{ {
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load"; string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load";
@ -30,7 +34,22 @@ namespace Bloxstrap
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine(LOG_IDENT, "Failed to load!"); App.Logger.WriteLine(LOG_IDENT, "Failed to load!");
App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}"); App.Logger.WriteException(LOG_IDENT, ex);
if (alertFailure)
{
string message = "";
if (ClassName == nameof(Settings))
message = Strings.JsonManager_SettingsLoadFailed;
else if (ClassName == nameof(FastFlagManager))
message = Strings.JsonManager_FastFlagsLoadFailed;
if (!String.IsNullOrEmpty(message))
Frontend.ShowMessageBox($"{message}\n\n{ex.Message}", System.Windows.MessageBoxImage.Warning);
}
Save();
} }
} }
@ -41,7 +60,21 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}..."); App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}...");
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!); Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);
try
{
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true })); File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
}
catch (IOException ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to save");
App.Logger.WriteException(LOG_IDENT, ex);
string errorMessage = string.Format(Resources.Strings.Bootstrapper_JsonManagerSaveFailed, ClassName, ex.Message);
Frontend.ShowMessageBox(errorMessage, System.Windows.MessageBoxImage.Warning);
return;
}
App.Logger.WriteLine(LOG_IDENT, "Save complete!"); App.Logger.WriteLine(LOG_IDENT, "Save complete!");
} }

View File

@ -1,11 +1,10 @@
using System.Windows; using System.Windows;
using Bloxstrap.UI.Elements.Dialogs;
using Microsoft.Win32;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Bloxstrap.UI.Elements.Dialogs;
namespace Bloxstrap namespace Bloxstrap
{ {
public static class LaunchHandler public static class LaunchHandler
@ -19,6 +18,7 @@ namespace Bloxstrap
break; break;
case NextAction.LaunchRoblox: case NextAction.LaunchRoblox:
App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player;
LaunchRoblox(); LaunchRoblox();
break; break;
@ -42,6 +42,8 @@ namespace Bloxstrap
LaunchRoblox(); LaunchRoblox();
else if (!App.LaunchSettings.QuietFlag.Active) else if (!App.LaunchSettings.QuietFlag.Active)
LaunchMenu(); LaunchMenu();
else
App.Terminate();
} }
public static void LaunchInstaller() public static void LaunchInstaller()
@ -51,6 +53,7 @@ namespace Bloxstrap
if (!interlock.IsAcquired) if (!interlock.IsAcquired)
{ {
Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Installer, MessageBoxImage.Stop); Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Installer, MessageBoxImage.Stop);
App.Terminate();
return; return;
} }
@ -95,6 +98,7 @@ namespace Bloxstrap
if (!interlock.IsAcquired) if (!interlock.IsAcquired)
{ {
Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Uninstaller, MessageBoxImage.Stop); Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Uninstaller, MessageBoxImage.Stop);
App.Terminate();
return; return;
} }
@ -115,11 +119,16 @@ namespace Bloxstrap
} }
if (!confirmed) if (!confirmed)
{
App.Terminate();
return; return;
}
Installer.DoUninstall(keepData); Installer.DoUninstall(keepData);
Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information); Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information);
App.Terminate();
} }
public static void LaunchSettings() public static void LaunchSettings()
@ -131,7 +140,9 @@ namespace Bloxstrap
if (interlock.IsAcquired) if (interlock.IsAcquired)
{ {
bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1; bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1;
new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).ShowDialog();
var window = new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning);
window.Show();
} }
else else
{ {
@ -156,7 +167,6 @@ namespace Bloxstrap
{ {
const string LOG_IDENT = "LaunchHandler::LaunchRoblox"; const string LOG_IDENT = "LaunchHandler::LaunchRoblox";
if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll"))) if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll")))
{ {
Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error); Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error);
@ -167,15 +177,6 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND); App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
} }
bool installWebView2 = false;
{
using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}");
using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}");
if (hklmKey is null && hkcuKey is null)
installWebView2 = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes) == MessageBoxResult.Yes;
}
if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _)) if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
{ {
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex // this currently doesn't work very well since it relies on checking the existence of the singleton mutex
@ -191,11 +192,9 @@ namespace Bloxstrap
} }
} }
App.NotifyIcon = new();
// start bootstrapper and show the bootstrapper modal if we're not running silently // start bootstrapper and show the bootstrapper modal if we're not running silently
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
var bootstrapper = new Bootstrapper(installWebView2); var bootstrapper = new Bootstrapper();
IBootstrapperDialog? dialog = null; IBootstrapperDialog? dialog = null;
if (!App.LaunchSettings.QuietFlag.Active) if (!App.LaunchSettings.QuietFlag.Active)
@ -206,45 +205,53 @@ namespace Bloxstrap
dialog.Bootstrapper = bootstrapper; dialog.Bootstrapper = bootstrapper;
} }
Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t => Task.Run(bootstrapper.Run).ContinueWith(t =>
{ {
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
// notifyicon is blocking main thread, must be disposed here
App.NotifyIcon?.Dispose();
if (t.IsFaulted) if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is null) if (t.Exception is not null)
return; App.FinalizeExceptionHandling(t.Exception);
}
App.Logger.WriteException(LOG_IDENT, t.Exception); App.Terminate();
Exception exception = t.Exception;
#if !DEBUG
if (t.Exception.GetType().ToString() == "System.AggregateException")
exception = t.Exception.InnerException!;
#endif
App.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
dialog?.ShowBootstrapper(); dialog?.ShowBootstrapper();
if (!App.LaunchSettings.NoLaunchFlag.Active && App.Settings.Prop.EnableActivityTracking)
App.NotifyIcon?.InitializeContextMenu();
App.Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");
bootstrapperTask.Wait();
} }
public static void LaunchWatcher() public static void LaunchWatcher()
{ {
const string LOG_IDENT = "LaunchHandler::LaunchWatcher";
// this whole topology is a bit confusing, bear with me:
// main thread: strictly UI only, handles showing of the notification area icon, context menu, server details dialog
// - server information task: queries server location, invoked if either the explorer notification is shown or the server details dialog is opened
// - discord rpc thread: handles rpc connection with discord
// - discord rich presence tasks: handles querying and displaying of game information, invoked on activity watcher events
// - watcher task: runs activity watcher + waiting for roblox to close, terminates when it has
var watcher = new Watcher();
Task.Run(watcher.Run).ContinueWith(t =>
{
App.Logger.WriteLine(LOG_IDENT, "Watcher task has finished");
watcher.Dispose();
if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the watcher");
if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception);
}
App.Terminate();
});
} }
} }
} }

View File

@ -28,7 +28,13 @@ namespace Bloxstrap
public LaunchFlag StudioFlag { get; } = new("studio"); public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchMode RobloxLaunchMode { get; private set; } = LaunchMode.None; #if DEBUG
public bool BypassUpdateCheck => true;
#else
public bool BypassUpdateCheck => UninstallFlag.Active || WatcherFlag.Active;
#endif
public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None;
public string RobloxLaunchArgs { get; private set; } = ""; public string RobloxLaunchArgs { get; private set; } = "";
@ -37,7 +43,7 @@ namespace Bloxstrap
/// </summary> /// </summary>
public string[] Args { get; private set; } public string[] Args { get; private set; }
private Dictionary<string, LaunchFlag> _flagMap = new(); private readonly Dictionary<string, LaunchFlag> _flagMap = new();
public LaunchSettings(string[] args) public LaunchSettings(string[] args)
{ {
@ -68,7 +74,7 @@ namespace Bloxstrap
string identifier = arg[1..]; string identifier = arg[1..];
if (_flagMap[identifier] is not LaunchFlag flag) if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
continue; continue;
flag.Active = true; flag.Active = true;

View File

@ -7,7 +7,7 @@
private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly SemaphoreSlim _semaphore = new(1, 1);
private FileStream? _filestream; private FileStream? _filestream;
public readonly List<string> Backlog = new(); public readonly List<string> History = new();
public bool Initialized = false; public bool Initialized = false;
public bool NoWriteMode = false; public bool NoWriteMode = false;
public string? FileLocation; public string? FileLocation;
@ -16,8 +16,7 @@
{ {
const string LOG_IDENT = "Logger::Initialize"; const string LOG_IDENT = "Logger::Initialize";
// TODO: <Temp>/Bloxstrap/Logs/ string directory = useTempDir ? Path.Combine(Paths.TempLogs) : Path.Combine(Paths.Base, "Logs");
string directory = useTempDir ? Path.Combine(Paths.LocalAppData, "Temp") : Path.Combine(Paths.Base, "Logs");
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
string filename = $"{App.ProjectName}_{timestamp}.log"; string filename = $"{App.ProjectName}_{timestamp}.log";
string location = Path.Combine(directory, filename); string location = Path.Combine(directory, filename);
@ -55,7 +54,7 @@
WriteLine(LOG_IDENT, $"Failed to initialize because Bloxstrap cannot write to {directory}"); WriteLine(LOG_IDENT, $"Failed to initialize because Bloxstrap cannot write to {directory}");
Frontend.ShowMessageBox( Frontend.ShowMessageBox(
String.Format(Resources.Strings.Logger_NoWriteMode, directory), String.Format(Strings.Logger_NoWriteMode, directory),
System.Windows.MessageBoxImage.Warning, System.Windows.MessageBoxImage.Warning,
System.Windows.MessageBoxButton.OK System.Windows.MessageBoxButton.OK
); );
@ -68,8 +67,8 @@
Initialized = true; Initialized = true;
if (Backlog.Count > 0) if (History.Count > 0)
WriteToLog(string.Join("\r\n", Backlog)); WriteToLog(string.Join("\r\n", History));
WriteLine(LOG_IDENT, "Finished initializing!"); WriteLine(LOG_IDENT, "Finished initializing!");
@ -102,10 +101,12 @@
{ {
string timestamp = DateTime.UtcNow.ToString("s") + "Z"; string timestamp = DateTime.UtcNow.ToString("s") + "Z";
string outcon = $"{timestamp} {message}"; string outcon = $"{timestamp} {message}";
string outlog = outcon.Replace(Paths.UserProfile, "%UserProfile%"); string outlog = outcon.Replace(Paths.UserProfile, "%UserProfile%", StringComparison.InvariantCultureIgnoreCase);
Debug.WriteLine(outcon); Debug.WriteLine(outcon);
WriteToLog(outlog); WriteToLog(outlog);
History.Add(outlog);
} }
public void WriteLine(string identifier, string message) => WriteLine($"[{identifier}] {message}"); public void WriteLine(string identifier, string message) => WriteLine($"[{identifier}] {message}");
@ -122,10 +123,7 @@
private async void WriteToLog(string message) private async void WriteToLog(string message)
{ {
if (!Initialized) if (!Initialized)
{
Backlog.Add(message);
return; return;
}
try try
{ {

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.Models.APIs.Config
{
public class Supporter
{
[JsonPropertyName("imageAsset")]
public string ImageAsset { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
public string Image => $"https://raw.githubusercontent.com/bloxstraplabs/config/main/assets/{ImageAsset}";
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.APIs.Config
{
public class SupporterData
{
[JsonPropertyName("columns")]
public int Columns { get; set; }
[JsonPropertyName("supporters")]
public List<Supporter> Supporters { get; set; } = null!;
}
}

View File

@ -0,0 +1,8 @@
public class GithubReleaseAsset
{
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
}

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models namespace Bloxstrap.Models.APIs.GitHub
{ {
public class GithubRelease public class GithubRelease
{ {
@ -17,13 +17,4 @@
[JsonPropertyName("assets")] [JsonPropertyName("assets")]
public List<GithubReleaseAsset>? Assets { get; set; } public List<GithubReleaseAsset>? Assets { get; set; }
} }
public class GithubReleaseAsset
{
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
}
} }

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models namespace Bloxstrap.Models.APIs
{ {
public class IPInfoResponse public class IPInfoResponse
{ {

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi namespace Bloxstrap.Models.APIs.Roblox
{ {
/// <summary> /// <summary>
/// Roblox.Web.WebAPI.Models.ApiArrayResponse /// Roblox.Web.WebAPI.Models.ApiArrayResponse

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models namespace Bloxstrap.Models.APIs.Roblox
{ {
public class ClientFlagSettings public class ClientFlagSettings
{ {

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models namespace Bloxstrap.Models.APIs.Roblox
{ {
public class ClientVersion public class ClientVersion
{ {

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi namespace Bloxstrap.Models.APIs.Roblox
{ {
/// <summary> /// <summary>
/// Roblox.Games.Api.Models.Response.GameCreator /// Roblox.Games.Api.Models.Response.GameCreator

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi namespace Bloxstrap.Models.APIs.Roblox
{ {
/// <summary> /// <summary>

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi namespace Bloxstrap.Models.APIs.Roblox
{ {
/// <summary> /// <summary>
/// Roblox.Web.Responses.Thumbnails.ThumbnailResponse /// Roblox.Web.Responses.Thumbnails.ThumbnailResponse

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi namespace Bloxstrap.Models.APIs.Roblox
{ {
// lmao its just one property // lmao its just one property
public class UniverseIdResponse public class UniverseIdResponse

View File

@ -0,0 +1,149 @@
using System.Web;
using System.Windows;
using System.Windows.Input;
using Bloxstrap.Models.APIs;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.Models.Entities
{
public class ActivityData
{
private long _universeId = 0;
/// <summary>
/// If the current activity stems from an in-universe teleport, then this will be
/// set to the activity that corresponds to the initial game join
/// </summary>
public ActivityData? RootActivity;
public long UniverseId
{
get => _universeId;
set
{
_universeId = value;
UniverseDetails.LoadFromCache(value);
}
}
public long PlaceId { get; set; } = 0;
public string JobId { get; set; } = string.Empty;
/// <summary>
/// This will be empty unless the server joined is a private server
/// </summary>
public string AccessCode { get; set; } = string.Empty;
public string MachineAddress { get; set; } = string.Empty;
public bool MachineAddressValid => !string.IsNullOrEmpty(MachineAddress) && !MachineAddress.StartsWith("10.");
public bool IsTeleport { get; set; } = false;
public ServerType ServerType { get; set; } = ServerType.Public;
public DateTime TimeJoined { get; set; }
public DateTime? TimeLeft { get; set; }
// everything below here is optional strictly for bloxstraprpc, discord rich presence, or game history
/// <summary>
/// This is intended only for other people to use, i.e. context menu invite link, rich presence joining
/// </summary>
public string RPCLaunchData { get; set; } = string.Empty;
public UniverseDetails? UniverseDetails { get; set; }
public string GameHistoryDescription
{
get
{
string desc = string.Format("{0} • {1} - {2}", UniverseDetails?.Data.Creator.Name, TimeJoined.ToString("h:mm tt"), TimeLeft?.ToString("h:mm tt"));
if (ServerType != ServerType.Public)
desc += " • " + ServerType.ToTranslatedString();
return desc;
}
}
public ICommand RejoinServerCommand => new RelayCommand(RejoinServer);
private SemaphoreSlim serverQuerySemaphore = new(1, 1);
public string GetInviteDeeplink(bool launchData = true)
{
string deeplink = $"roblox://experiences/start?placeId={PlaceId}";
if (ServerType == ServerType.Private)
deeplink += "&accessCode=" + AccessCode;
else
deeplink += "&gameInstanceId=" + JobId;
if (launchData && !string.IsNullOrEmpty(RPCLaunchData))
deeplink += "&launchData=" + HttpUtility.UrlEncode(RPCLaunchData);
return deeplink;
}
public async Task<string?> QueryServerLocation()
{
const string LOG_IDENT = "ActivityData::QueryServerLocation";
if (!MachineAddressValid)
throw new InvalidOperationException($"Machine address is invalid ({MachineAddress})");
await serverQuerySemaphore.WaitAsync();
if (GlobalCache.ServerLocation.TryGetValue(MachineAddress, out string? location))
{
serverQuerySemaphore.Release();
return location;
}
try
{
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{MachineAddress}/json");
if (string.IsNullOrEmpty(ipInfo.City))
throw new InvalidHTTPResponseException("Reported city was blank");
if (ipInfo.City == ipInfo.Region)
location = $"{ipInfo.Region}, {ipInfo.Country}";
else
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
GlobalCache.ServerLocation[MachineAddress] = location;
serverQuerySemaphore.Release();
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {MachineAddress}");
App.Logger.WriteException(LOG_IDENT, ex);
GlobalCache.ServerLocation[MachineAddress] = location;
serverQuerySemaphore.Release();
Frontend.ShowConnectivityDialog(
string.Format(Strings.Dialog_Connectivity_UnableToConnect, "ipinfo.io"),
Strings.ActivityWatcher_LocationQueryFailed,
MessageBoxImage.Warning,
ex
);
}
return location;
}
public override string ToString() => $"{PlaceId}/{JobId}";
private void RejoinServer()
{
string playerPath = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe");
Process.Start(playerPath, GetInviteDeeplink(false));
}
}
}

View File

@ -1,7 +1,7 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Windows.Markup; using System.Windows.Markup;
namespace Bloxstrap.Models namespace Bloxstrap.Models.Entities
{ {
public class ModPresetFileData public class ModPresetFileData
{ {

View File

@ -0,0 +1,52 @@
using Bloxstrap.Models.APIs.Roblox;
namespace Bloxstrap.Models.Entities
{
public class UniverseDetails
{
private static List<UniverseDetails> _cache { get; set; } = new();
public GameDetailResponse Data { get; set; } = null!;
/// <summary>
/// Returns data for a 128x128 icon
/// </summary>
public ThumbnailResponse Thumbnail { get; set; } = null!;
public static UniverseDetails? LoadFromCache(long id)
{
var cacheQuery = _cache.Where(x => x.Data?.Id == id);
if (cacheQuery.Any())
return cacheQuery.First();
return null;
}
public static Task FetchSingle(long id) => FetchBulk(id.ToString());
public static async Task FetchBulk(string ids)
{
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={ids}");
if (!gameDetailResponse.Data.Any())
throw new InvalidHTTPResponseException("Roblox API for Game Details returned invalid data");
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={ids}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
if (!universeThumbnailResponse.Data.Any())
throw new InvalidHTTPResponseException("Roblox API for Game Thumbnails returned invalid data");
foreach (string strId in ids.Split(','))
{
long id = long.Parse(strId);
_cache.Add(new UniverseDetails
{
Data = gameDetailResponse.Data.Where(x => x.Id == id).First(),
Thumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == id).First(),
});
}
}
}
}

View File

@ -9,10 +9,15 @@ namespace Bloxstrap.Models.Manifest
public class Package public class Package
{ {
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Signature { get; set; } = ""; public string Signature { get; set; } = "";
public int PackedSize { get; set; } public int PackedSize { get; set; }
public int Size { get; set; } public int Size { get; set; }
public string DownloadPath => Path.Combine(Paths.Downloads, Signature);
public override string ToString() public override string ToString()
{ {
return $"[{Signature}] {Name}"; return $"[{Signature}] {Name}";

View File

@ -8,9 +8,9 @@ namespace Bloxstrap.Models.Manifest
{ {
public class PackageManifest : List<Package> public class PackageManifest : List<Package>
{ {
private PackageManifest(string data) public PackageManifest(string data)
{ {
using StringReader reader = new StringReader(data); using var reader = new StringReader(data);
string? version = reader.ReadLine(); string? version = reader.ReadLine();
if (version != "v0") if (version != "v0")
@ -46,13 +46,5 @@ namespace Bloxstrap.Models.Manifest
}); });
} }
} }
public static async Task<PackageManifest> Get(string versionGuid)
{
string pkgManifestUrl = RobloxDeployment.GetLocation($"/{versionGuid}-rbxPkgManifest.txt");
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
return new PackageManifest(pkgManifestData);
}
} }
} }

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.Persistable
{
public class AppState
{
public string VersionGuid { get; set; } = string.Empty;
public Dictionary<string, string> PackageHashes { get; set; } = new();
public int Size { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Bloxstrap.Models namespace Bloxstrap.Models.Persistable
{ {
public class Settings public class Settings
{ {
@ -25,6 +25,5 @@ namespace Bloxstrap.Models
// mod preset configuration // mod preset configuration
public bool UseDisableAppPatch { get; set; } = false; public bool UseDisableAppPatch { get; set; } = false;
public bool DisableFullscreenOptimizations { get; set; } = false;
} }
} }

View File

@ -0,0 +1,17 @@
namespace Bloxstrap.Models.Persistable
{
public class State
{
public bool ShowFFlagEditorWarning { get; set; } = true;
public bool PromptWebView2Install { get; set; } = true;
public AppState Player { get; set; } = new();
public AppState Studio { get; set; } = new();
public WindowState SettingsWindow { get; set; } = new();
public List<string> ModManifest { get; set; } = new();
}
}

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.Models.Persistable
{
public class WindowState
{
public double Width { get; set; }
public double Height { get; set; }
public double Left { get; set; }
public double Top { get; set; }
}
}

View File

@ -29,12 +29,16 @@ namespace Bloxstrap.Models.SettingTasks.Base
set set
{ {
App.PendingSettingTasks[Name] = this;
_newState = value; _newState = value;
if (Changed)
App.PendingSettingTasks[Name] = this;
else
App.PendingSettingTasks.Remove(Name);
} }
} }
public override bool Changed => NewState != OriginalState; public override bool Changed => _newState != OriginalState;
public BoolBaseTask(string prefix, string name) : base(prefix, name) { } public BoolBaseTask(string prefix, string name) : base(prefix, name) { }
} }

View File

@ -23,12 +23,16 @@
set set
{ {
App.PendingSettingTasks[Name] = this;
_newState = value; _newState = value;
if (Changed)
App.PendingSettingTasks[Name] = this;
else
App.PendingSettingTasks.Remove(Name);
} }
} }
public override bool Changed => !NewState.Equals(OriginalState); public override bool Changed => !_newState.Equals(OriginalState);
public IEnumerable<T> Selections { get; private set; } public IEnumerable<T> Selections { get; private set; }
= Enum.GetValues(typeof(T)).Cast<T>().OrderBy(x => = Enum.GetValues(typeof(T)).Cast<T>().OrderBy(x =>

View File

@ -29,12 +29,16 @@ namespace Bloxstrap.Models.SettingTasks.Base
set set
{ {
App.PendingSettingTasks[Name] = this;
_newState = value; _newState = value;
if (Changed)
App.PendingSettingTasks[Name] = this;
else
App.PendingSettingTasks.Remove(Name);
} }
} }
public override bool Changed => NewState != OriginalState; public override bool Changed => _newState != OriginalState;
public StringBaseTask(string prefix, string name) : base(prefix, name) { } public StringBaseTask(string prefix, string name) : base(prefix, name) { }
} }

View File

@ -52,9 +52,12 @@ namespace Bloxstrap.Models.SettingTasks
{ {
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox( Frontend.ShowConnectivityDialog(
String.Format(Strings.Menu_Mods_Presets_EmojiType_Error, ex.Message), String.Format(Strings.Dialog_Connectivity_UnableToConnect, "GitHub"),
MessageBoxImage.Warning); $"{Strings.Menu_Mods_Presets_EmojiType_Error}\n\n{Strings.Dialog_Connectivity_TryAgainLater}",
MessageBoxImage.Warning,
ex
);
} }
} }
else if (query is not null && query.Any()) else if (query is not null && query.Any())

View File

@ -1,4 +1,5 @@
using Bloxstrap.Models.SettingTasks.Base; using Bloxstrap.Models.Entities;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks namespace Bloxstrap.Models.SettingTasks
{ {

View File

@ -1,4 +1,5 @@
using Bloxstrap.Models.SettingTasks.Base; using Bloxstrap.Models.Entities;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks namespace Bloxstrap.Models.SettingTasks
{ {

View File

@ -1,17 +0,0 @@
namespace Bloxstrap.Models
{
public class State
{
public bool ShowFFlagEditorWarning { get; set; } = true;
[Obsolete("Use PlayerVersionGuid instead", true)]
public string VersionGuid { set { PlayerVersionGuid = value; } }
public string PlayerVersionGuid { get; set; } = "";
public string StudioVersionGuid { get; set; } = "";
public int PlayerSize { get; set; } = 0;
public int StudioSize { get; set; } = 0;
public List<string> ModManifest { get; set; } = new();
}
}

View File

@ -2,4 +2,4 @@
FlashWindow FlashWindow
GetWindowLong GetWindowLong
SetWindowLong SetWindowLong
EnumDisplaySettings SHObjectProperties

View File

@ -4,6 +4,7 @@
{ {
// note that these are directories that aren't tethered to the basedirectory // note that these are directories that aren't tethered to the basedirectory
// so these can safely be called before initialization // so these can safely be called before initialization
public static string Temp => Path.Combine(Path.GetTempPath(), App.ProjectName);
public static string UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); public static string UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
@ -12,12 +13,15 @@
public static string Process => Environment.ProcessPath!; public static string Process => Environment.ProcessPath!;
public static string TempUpdates => Path.Combine(Temp, "Updates");
public static string TempLogs => Path.Combine(Temp, "Logs");
public static string Base { get; private set; } = ""; public static string Base { get; private set; } = "";
public static string Downloads { get; private set; } = ""; public static string Downloads { get; private set; } = "";
public static string Logs { get; private set; } = ""; public static string Logs { get; private set; } = "";
public static string Integrations { get; private set; } = ""; public static string Integrations { get; private set; } = "";
public static string Versions { get; private set; } = "";
public static string Modifications { get; private set; } = ""; public static string Modifications { get; private set; } = "";
public static string Roblox { get; private set; } = "";
public static string Application { get; private set; } = ""; public static string Application { get; private set; } = "";
@ -31,8 +35,8 @@
Downloads = Path.Combine(Base, "Downloads"); Downloads = Path.Combine(Base, "Downloads");
Logs = Path.Combine(Base, "Logs"); Logs = Path.Combine(Base, "Logs");
Integrations = Path.Combine(Base, "Integrations"); Integrations = Path.Combine(Base, "Integrations");
Versions = Path.Combine(Base, "Versions");
Modifications = Path.Combine(Base, "Modifications"); Modifications = Path.Combine(Base, "Modifications");
Roblox = Path.Combine(Base, "Roblox");
Application = Path.Combine(Base, $"{App.ProjectName}.exe"); Application = Path.Combine(Base, $"{App.ProjectName}.exe");
} }

View File

@ -26,6 +26,10 @@
"Bloxstrap (Studio Launch)": { "Bloxstrap (Studio Launch)": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "-studio" "commandLineArgs": "-studio"
},
"Bloxstrap (Watcher)": {
"commandName": "Project",
"commandLineArgs": "-watcher"
} }
} }
} }

View File

@ -61,25 +61,70 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to left game. /// Looks up a localized string similar to Licenses.
/// </summary> /// </summary>
public static string ActivityTracker_LeftGame { public static string About_Licenses_Title {
get { get {
return ResourceManager.GetString("ActivityTracker.LeftGame", resourceCulture); return ResourceManager.GetString("About.Licenses.Title", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to lookup failed. /// Looks up a localized string similar to These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!.
/// </summary> /// </summary>
public static string ActivityTracker_LookupFailed { public static string About_Supporters_Description {
get { get {
return ResourceManager.GetString("ActivityTracker.LookupFailed", resourceCulture); return ResourceManager.GetString("About.Supporters.Description", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page.. /// Looks up a localized string similar to Supporters.
/// </summary>
public static string About_Supporters_Title {
get {
return ResourceManager.GetString("About.Supporters.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to About Bloxstrap.
/// </summary>
public static string About_Title {
get {
return ResourceManager.GetString("About.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Translators.
/// </summary>
public static string About_Translators_Title {
get {
return ResourceManager.GetString("About.Translators.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The server location could not be queried. You may be joining games too quickly..
/// </summary>
public static string ActivityWatcher_LocationQueryFailed {
get {
return ResourceManager.GetString("ActivityWatcher.LocationQueryFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your current game will not show on your Discord presence because an error occurred when loading the game information..
/// </summary>
public static string ActivityWatcher_RichPresenceLoadFailed {
get {
return ResourceManager.GetString("ActivityWatcher.RichPresenceLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website..
/// </summary> /// </summary>
public static string Bootstrapper_AutoUpdateFailed { public static string Bootstrapper_AutoUpdateFailed {
get { get {
@ -96,33 +141,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to It&apos;s possible that something is preventing Bloxstrap from connecting to the internet. Please check and try again..
/// </summary>
public static string Bootstrapper_Connectivity_Preventing {
get {
return ResourceManager.GetString("Bootstrapper.Connectivity.Preventing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox may be down right now. See status.roblox.com for more information. Please try again later..
/// </summary>
public static string Bootstrapper_Connectivity_RobloxDown {
get {
return ResourceManager.GetString("Bootstrapper.Connectivity.RobloxDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap timed out when trying to connect to three different Roblox deployment mirrors, indicating a poor internet connection. Please try again later..
/// </summary>
public static string Bootstrapper_Connectivity_TimedOut {
get {
return ResourceManager.GetString("Bootstrapper.Connectivity.TimedOut", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu.. /// Looks up a localized string similar to Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu..
/// </summary> /// </summary>
@ -152,6 +170,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Failed to save {0}: {1}.
/// </summary>
public static string Bootstrapper_JsonManagerSaveFailed {
get {
return ResourceManager.GetString("Bootstrapper.JsonManagerSaveFailed", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Bloxstrap does not have enough disk space to download and install Roblox. Please free up some disk space and try again.. /// Looks up a localized string similar to Bloxstrap does not have enough disk space to download and install Roblox. Please free up some disk space and try again..
/// </summary> /// </summary>
@ -423,11 +450,11 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Locate log file. /// Looks up a localized string similar to Loading, please wait....
/// </summary> /// </summary>
public static string Common_LocateLogFile { public static string Common_Loading {
get { get {
return ResourceManager.GetString("Common.LocateLogFile", resourceCulture); return ResourceManager.GetString("Common.Loading", resourceCulture);
} }
} }
@ -467,6 +494,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Could not load data because of a network error..
/// </summary>
public static string Common_NetworkError {
get {
return ResourceManager.GetString("Common.NetworkError", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to New. /// Looks up a localized string similar to New.
/// </summary> /// </summary>
@ -485,6 +521,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Not available.
/// </summary>
public static string Common_NotAvailable {
get {
return ResourceManager.GetString("Common.NotAvailable", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to OK. /// Looks up a localized string similar to OK.
/// </summary> /// </summary>
@ -494,6 +539,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Open log file.
/// </summary>
public static string Common_OpenLogFile {
get {
return ResourceManager.GetString("Common.OpenLogFile", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Presets. /// Looks up a localized string similar to Presets.
/// </summary> /// </summary>
@ -512,6 +566,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option..
/// </summary>
public static string Common_RobloxNotInstalled {
get {
return ResourceManager.GetString("Common.RobloxNotInstalled", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Shortcuts. /// Looks up a localized string similar to Shortcuts.
/// </summary> /// </summary>
@ -603,11 +666,29 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Open log file. /// Looks up a localized string similar to Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them..
/// </summary> /// </summary>
public static string ContextMenu_OpenLogFile { public static string ContextMenu_GameHistory_Description {
get { get {
return ResourceManager.GetString("ContextMenu.OpenLogFile", resourceCulture); return ResourceManager.GetString("ContextMenu.GameHistory.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rejoin.
/// </summary>
public static string ContextMenu_GameHistory_Rejoin {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Rejoin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game history.
/// </summary>
public static string ContextMenu_GameHistory_Title {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Title", resourceCulture);
} }
} }
@ -620,15 +701,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to See server details.
/// </summary>
public static string ContextMenu_SeeServerDetails {
get {
return ResourceManager.GetString("ContextMenu.SeeServerDetails", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Copy Instance ID. /// Looks up a localized string similar to Copy Instance ID.
/// </summary> /// </summary>
@ -647,15 +719,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Loading, please wait....
/// </summary>
public static string ContextMenu_ServerInformation_Loading {
get {
return ResourceManager.GetString("ContextMenu.ServerInformation.Loading", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Location. /// Looks up a localized string similar to Location.
/// </summary> /// </summary>
@ -765,6 +828,60 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Something is likely preventing Bloxstrap from connecting to the internet..
/// </summary>
public static string Dialog_Connectivity_Preventing {
get {
return ResourceManager.GetString("Dialog.Connectivity.Preventing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox may be down right now. See {0} for more information..
/// </summary>
public static string Dialog_Connectivity_RobloxDown {
get {
return ResourceManager.GetString("Dialog.Connectivity.RobloxDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue..
/// </summary>
public static string Dialog_Connectivity_RobloxUpgradeNeeded {
get {
return ResourceManager.GetString("Dialog.Connectivity.RobloxUpgradeNeeded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to For this launch, Roblox will not be checked for upgrades, and changes to mods will not be applied..
/// </summary>
public static string Dialog_Connectivity_RobloxUpgradeSkip {
get {
return ResourceManager.GetString("Dialog.Connectivity.RobloxUpgradeSkip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} may be down right now..
/// </summary>
public static string Dialog_Connectivity_ServiceDown {
get {
return ResourceManager.GetString("Dialog.Connectivity.ServiceDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The connection timed out, which could indicate a poor internet connection or a firewall block..
/// </summary>
public static string Dialog_Connectivity_TimedOut {
get {
return ResourceManager.GetString("Dialog.Connectivity.TimedOut", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Connectivity error. /// Looks up a localized string similar to Connectivity error.
/// </summary> /// </summary>
@ -775,7 +892,16 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Bloxstrap is unable to connect to Roblox. /// Looks up a localized string similar to Please try again later..
/// </summary>
public static string Dialog_Connectivity_TryAgainLater {
get {
return ResourceManager.GetString("Dialog.Connectivity.TryAgainLater", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap is unable to connect to {0}.
/// </summary> /// </summary>
public static string Dialog_Connectivity_UnableToConnect { public static string Dialog_Connectivity_UnableToConnect {
get { get {
@ -850,6 +976,17 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to The chosen bootstrapper icon could not be loaded.
///
///{0}.
/// </summary>
public static string Dialog_IconLoadFailed {
get {
return ResourceManager.GetString("Dialog.IconLoadFailed", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Choose preferred language. /// Looks up a localized string similar to Choose preferred language.
/// </summary> /// </summary>
@ -868,6 +1005,33 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Roblox has crashed..
/// </summary>
public static string Dialog_PlayerError_Crash {
get {
return ResourceManager.GetString("Dialog.PlayerError.Crash", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox failed to launch..
/// </summary>
public static string Dialog_PlayerError_FailedLaunch {
get {
return ResourceManager.GetString("Dialog.PlayerError.FailedLaunch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please read the following help information, which will open in your web browser when you close this dialog..
/// </summary>
public static string Dialog_PlayerError_HelpInformation {
get {
return ResourceManager.GetString("Dialog.PlayerError.HelpInformation", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Early 2015. /// Looks up a localized string similar to Early 2015.
/// </summary> /// </summary>
@ -1139,7 +1303,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Private. /// Looks up a localized string similar to Private server.
/// </summary> /// </summary>
public static string Enums_ServerType_Private { public static string Enums_ServerType_Private {
get { get {
@ -1148,7 +1312,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Public. /// Looks up a localized string similar to Public server.
/// </summary> /// </summary>
public static string Enums_ServerType_Public { public static string Enums_ServerType_Public {
get { get {
@ -1157,7 +1321,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Reserved. /// Looks up a localized string similar to Reserved server.
/// </summary> /// </summary>
public static string Enums_ServerType_Reserved { public static string Enums_ServerType_Reserved {
get { get {
@ -1249,6 +1413,17 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to The version of Bloxstrap you&apos;ve launched is older than the version you currently have installed.
///Issues may occur and your settings may be altered. A reinstall is recommended.
///Are you sure you want to continue?.
/// </summary>
public static string InstallChecker_VersionLessThanInstalled {
get {
return ResourceManager.GetString("InstallChecker.VersionLessThanInstalled", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Will drop you into the desktop app once everything&apos;s done. /// Looks up a localized string similar to Will drop you into the desktop app once everything&apos;s done.
/// </summary> /// </summary>
@ -1309,6 +1484,17 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Bloxstrap has been installed to this location before and is still present, however the installer cannot overwrite the old executable.
///
///Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards..
/// </summary>
public static string Installer_Install_CannotOverwrite {
get {
return ResourceManager.GetString("Installer.Install.CannotOverwrite", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Existing data found. Your mods and settings will be restored.. /// Looks up a localized string similar to Existing data found. Your mods and settings will be restored..
/// </summary> /// </summary>
@ -1354,6 +1540,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Are you sure you want to cancel the installation?.
/// </summary>
public static string Installer_ShouldCancel {
get {
return ResourceManager.GetString("Installer.ShouldCancel", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Bloxstrap Installer. /// Looks up a localized string similar to Bloxstrap Installer.
/// </summary> /// </summary>
@ -1366,7 +1561,7 @@ namespace Bloxstrap.Resources {
/// <summary> /// <summary>
/// Looks up a localized string similar to Thank you for downloading Bloxstrap. /// Looks up a localized string similar to Thank you for downloading Bloxstrap.
/// ///
///You should have gotten it from either {0} or {1}. Those are the only official websites to get it from. ///You should have downloaded it from either {0} or {1}. Those are the only official websites to get it from. It is your responsibility to ensure you download from an official source.
/// ///
///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap&apos;s settings after installation.. ///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap&apos;s settings after installation..
/// </summary> /// </summary>
@ -1403,6 +1598,24 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Your Fast Flags could not be loaded. They have been reset to the default configuration..
/// </summary>
public static string JsonManager_FastFlagsLoadFailed {
get {
return ResourceManager.GetString("JsonManager.FastFlagsLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your Settings could not be loaded. They have been reset to the default configuration..
/// </summary>
public static string JsonManager_SettingsLoadFailed {
get {
return ResourceManager.GetString("JsonManager.SettingsLoadFailed", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Configure settings. /// Looks up a localized string similar to Configure settings.
/// </summary> /// </summary>
@ -2151,24 +2364,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Allows you to configure 21 different quality levels instead of 10..
/// </summary>
public static string Menu_FastFlags_Presets_AltGraphicsSelector_Description {
get {
return ResourceManager.GetString("Menu.FastFlags.Presets.AltGraphicsSelector.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use advanced graphics quality selector.
/// </summary>
public static string Menu_FastFlags_Presets_AltGraphicsSelector_Title {
get {
return ResourceManager.GetString("Menu.FastFlags.Presets.AltGraphicsSelector.Title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Rendering and Graphics. /// Looks up a localized string similar to Rendering and Graphics.
/// </summary> /// </summary>
@ -2472,15 +2667,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to e.g. C:\Windows\System32\cmd.exe.
/// </summary>
public static string Menu_Integrations_Custom_AppLocation_Placeholder {
get {
return ResourceManager.GetString("Menu.Integrations.Custom.AppLocation.Placeholder", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Auto close when Roblox closes. /// Looks up a localized string similar to Auto close when Roblox closes.
/// </summary> /// </summary>
@ -2509,7 +2695,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to e.g. /k echo Roblox is running!. /// Looks up a localized string similar to Roblox is running!.
/// </summary> /// </summary>
public static string Menu_Integrations_Custom_LaunchArgs_Placeholder { public static string Menu_Integrations_Custom_LaunchArgs_Placeholder {
get { get {
@ -2590,7 +2776,25 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running.. /// Looks up a localized string similar to When in-game, you&apos;ll be able to see where your server is located via [ipinfo.io]({0})..
/// </summary>
public static string Menu_Integrations_QueryServerLocation_Description {
get {
return ResourceManager.GetString("Menu.Integrations.QueryServerLocation.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Query server location.
/// </summary>
public static string Menu_Integrations_QueryServerLocation_Title {
get {
return ResourceManager.GetString("Menu.Integrations.QueryServerLocation.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running. [Find out more]({0})..
/// </summary> /// </summary>
public static string Menu_Integrations_RequiresActivityTracking { public static string Menu_Integrations_RequiresActivityTracking {
get { get {
@ -2616,24 +2820,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to When you join a game, you&apos;ll be notified of where your server&apos;s located. Won&apos;t show in fullscreen..
/// </summary>
public static string Menu_Integrations_ShowServerDetails_Description {
get {
return ResourceManager.GetString("Menu.Integrations.ShowServerDetails.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to See server location when joining a game.
/// </summary>
public static string Menu_Integrations_ShowServerDetails_Title {
get {
return ResourceManager.GetString("Menu.Integrations.ShowServerDetails.Title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Integrations. /// Looks up a localized string similar to Integrations.
/// </summary> /// </summary>
@ -2661,6 +2847,24 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Configure application parameters such as DPI scaling behaviour and [fullscreen optimizations]({0})..
/// </summary>
public static string Menu_Mods_Misc_CompatibilitySettings_Description {
get {
return ResourceManager.GetString("Menu.Mods.Misc.CompatibilitySettings.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Manage compatibility settings.
/// </summary>
public static string Menu_Mods_Misc_CompatibilitySettings_Title {
get {
return ResourceManager.GetString("Menu.Mods.Misc.CompatibilitySettings.Title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Choose font.... /// Looks up a localized string similar to Choose font....
/// </summary> /// </summary>
@ -2752,9 +2956,7 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to The emoji mod could not be applied because of a network error during download. /// Looks up a localized string similar to The emoji mod can not be applied at this time..
///
///{0}.
/// </summary> /// </summary>
public static string Menu_Mods_Presets_EmojiType_Error { public static string Menu_Mods_Presets_EmojiType_Error {
get { get {
@ -2942,6 +3144,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to You have unsaved changes. Are you sure you want to close without saving?.
/// </summary>
public static string Menu_UnsavedChanges {
get {
return ResourceManager.GetString("Menu.UnsavedChanges", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to They&apos;ll be kept where Bloxstrap was installed, and will automatically be restored on a reinstall.. /// Looks up a localized string similar to They&apos;ll be kept where Bloxstrap was installed, and will automatically be restored on a reinstall..
/// </summary> /// </summary>

View File

@ -117,26 +117,20 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ActivityTracker.LeftGame" xml:space="preserve">
<value>left game</value>
</data>
<data name="ActivityTracker.LookupFailed" xml:space="preserve">
<value>lookup failed</value>
</data>
<data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve"> <data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve">
<value>Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page.</value> <value>Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website.</value>
</data> </data>
<data name="Bootstrapper.ConfirmLaunch" xml:space="preserve"> <data name="Bootstrapper.ConfirmLaunch" xml:space="preserve">
<value>Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching?</value> <value>Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching?</value>
</data> </data>
<data name="Bootstrapper.Connectivity.Preventing" xml:space="preserve"> <data name="Dialog.Connectivity.Preventing" xml:space="preserve">
<value>It's possible that something is preventing Bloxstrap from connecting to the internet. Please check and try again.</value> <value>Something is likely preventing Bloxstrap from connecting to the internet.</value>
</data> </data>
<data name="Bootstrapper.Connectivity.RobloxDown" xml:space="preserve"> <data name="Dialog.Connectivity.RobloxDown" xml:space="preserve">
<value>Roblox may be down right now. See status.roblox.com for more information. Please try again later.</value> <value>Roblox may be down right now. See {0} for more information.</value>
</data> </data>
<data name="Bootstrapper.Connectivity.TimedOut" xml:space="preserve"> <data name="Dialog.Connectivity.TimedOut" xml:space="preserve">
<value>Bloxstrap timed out when trying to connect to three different Roblox deployment mirrors, indicating a poor internet connection. Please try again later.</value> <value>The connection timed out, which could indicate a poor internet connection or a firewall block.</value>
</data> </data>
<data name="Bootstrapper.EmojiPresetFetchFailed" xml:space="preserve"> <data name="Bootstrapper.EmojiPresetFetchFailed" xml:space="preserve">
<value>Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu.</value> <value>Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu.</value>
@ -226,8 +220,8 @@ Your ReShade configuration files will still be saved, and you can locate them by
<data name="Common.ImportJson" xml:space="preserve"> <data name="Common.ImportJson" xml:space="preserve">
<value>Import JSON</value> <value>Import JSON</value>
</data> </data>
<data name="Common.LocateLogFile" xml:space="preserve"> <data name="Common.OpenLogFile" xml:space="preserve">
<value>Locate log file</value> <value>Open log file</value>
</data> </data>
<data name="Common.Miscellaneous" xml:space="preserve"> <data name="Common.Miscellaneous" xml:space="preserve">
<value>Miscellaneous</value> <value>Miscellaneous</value>
@ -268,21 +262,12 @@ Your ReShade configuration files will still be saved, and you can locate them by
<data name="ContextMenu.CopyDeeplinkInvite" xml:space="preserve"> <data name="ContextMenu.CopyDeeplinkInvite" xml:space="preserve">
<value>Copy invite deeplink</value> <value>Copy invite deeplink</value>
</data> </data>
<data name="ContextMenu.OpenLogFile" xml:space="preserve">
<value>Open log file</value>
</data>
<data name="ContextMenu.SeeServerDetails" xml:space="preserve">
<value>See server details</value>
</data>
<data name="ContextMenu.ServerInformation.CopyInstanceId" xml:space="preserve"> <data name="ContextMenu.ServerInformation.CopyInstanceId" xml:space="preserve">
<value>Copy Instance ID</value> <value>Copy Instance ID</value>
</data> </data>
<data name="ContextMenu.ServerInformation.InstanceId" xml:space="preserve"> <data name="ContextMenu.ServerInformation.InstanceId" xml:space="preserve">
<value>Instance ID</value> <value>Instance ID</value>
</data> </data>
<data name="ContextMenu.ServerInformation.Loading" xml:space="preserve">
<value>Loading, please wait...</value>
</data>
<data name="ContextMenu.ServerInformation.Location" xml:space="preserve"> <data name="ContextMenu.ServerInformation.Location" xml:space="preserve">
<value>Location</value> <value>Location</value>
</data> </data>
@ -309,7 +294,7 @@ Click for more information</value>
<value>Connectivity error</value> <value>Connectivity error</value>
</data> </data>
<data name="Dialog.Connectivity.UnableToConnect" xml:space="preserve"> <data name="Dialog.Connectivity.UnableToConnect" xml:space="preserve">
<value>Bloxstrap is unable to connect to Roblox</value> <value>Bloxstrap is unable to connect to {0}</value>
</data> </data>
<data name="Dialog.Exception.CopyLogContents" xml:space="preserve"> <data name="Dialog.Exception.CopyLogContents" xml:space="preserve">
<value>Copy log contents</value> <value>Copy log contents</value>
@ -404,13 +389,13 @@ If not, then please report this exception through a [GitHub issue]({1}) along wi
<value>Direct3D 11</value> <value>Direct3D 11</value>
</data> </data>
<data name="Enums.ServerType.Private" xml:space="preserve"> <data name="Enums.ServerType.Private" xml:space="preserve">
<value>Private</value> <value>Private server</value>
</data> </data>
<data name="Enums.ServerType.Public" xml:space="preserve"> <data name="Enums.ServerType.Public" xml:space="preserve">
<value>Public</value> <value>Public server</value>
</data> </data>
<data name="Enums.ServerType.Reserved" xml:space="preserve"> <data name="Enums.ServerType.Reserved" xml:space="preserve">
<value>Reserved</value> <value>Reserved server</value>
</data> </data>
<data name="Enums.Theme.Dark" xml:space="preserve"> <data name="Enums.Theme.Dark" xml:space="preserve">
<value>Dark</value> <value>Dark</value>
@ -652,12 +637,6 @@ Do NOT use this to import large "flag lists" made by other people that promise t
<value>Learn more about Fast Flags, what these presets do, and how to use them.</value> <value>Learn more about Fast Flags, what these presets do, and how to use them.</value>
<comment>Title is Common.Help</comment> <comment>Title is Common.Help</comment>
</data> </data>
<data name="Menu.FastFlags.Presets.AltGraphicsSelector.Description" xml:space="preserve">
<value>Allows you to configure 21 different quality levels instead of 10.</value>
</data>
<data name="Menu.FastFlags.Presets.AltGraphicsSelector.Title" xml:space="preserve">
<value>Use advanced graphics quality selector</value>
</data>
<data name="Menu.FastFlags.Presets.D3DExclusiveFullscreenInfo" xml:space="preserve"> <data name="Menu.FastFlags.Presets.D3DExclusiveFullscreenInfo" xml:space="preserve">
<value>Direct3D [exclusive fullscreen]({0}) using Alt+Enter is enabled by default.</value> <value>Direct3D [exclusive fullscreen]({0}) using Alt+Enter is enabled by default.</value>
</data> </data>
@ -730,9 +709,6 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.Integrations.Custom.AppLocation" xml:space="preserve"> <data name="Menu.Integrations.Custom.AppLocation" xml:space="preserve">
<value>Application Location</value> <value>Application Location</value>
</data> </data>
<data name="Menu.Integrations.Custom.AppLocation.Placeholder" xml:space="preserve">
<value>e.g. C:\Windows\System32\cmd.exe</value>
</data>
<data name="Menu.Integrations.Custom.AutoClose" xml:space="preserve"> <data name="Menu.Integrations.Custom.AutoClose" xml:space="preserve">
<value>Auto close when Roblox closes</value> <value>Auto close when Roblox closes</value>
</data> </data>
@ -743,7 +719,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Launch Arguments</value> <value>Launch Arguments</value>
</data> </data>
<data name="Menu.Integrations.Custom.LaunchArgs.Placeholder" xml:space="preserve"> <data name="Menu.Integrations.Custom.LaunchArgs.Placeholder" xml:space="preserve">
<value>e.g. /k echo Roblox is running!</value> <value>Roblox is running!</value>
</data> </data>
<data name="Menu.Integrations.Custom.NewIntegration" xml:space="preserve"> <data name="Menu.Integrations.Custom.NewIntegration" xml:space="preserve">
<value>New Integration</value> <value>New Integration</value>
@ -770,7 +746,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Enable activity tracking</value> <value>Enable activity tracking</value>
</data> </data>
<data name="Menu.Integrations.RequiresActivityTracking" xml:space="preserve"> <data name="Menu.Integrations.RequiresActivityTracking" xml:space="preserve">
<value>This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running.</value> <value>This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running. [Find out more]({0}).</value>
</data> </data>
<data name="Menu.Integrations.ShowGameActivity.Description" xml:space="preserve"> <data name="Menu.Integrations.ShowGameActivity.Description" xml:space="preserve">
<value>The Roblox game you're playing will be shown on your Discord profile. [Not working?]({0})</value> <value>The Roblox game you're playing will be shown on your Discord profile. [Not working?]({0})</value>
@ -778,11 +754,11 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.Integrations.ShowGameActivity.Title" xml:space="preserve"> <data name="Menu.Integrations.ShowGameActivity.Title" xml:space="preserve">
<value>Show game activity</value> <value>Show game activity</value>
</data> </data>
<data name="Menu.Integrations.ShowServerDetails.Description" xml:space="preserve"> <data name="Menu.Integrations.QueryServerLocation.Description" xml:space="preserve">
<value>When you join a game, you'll be notified of where your server's located. Won't show in fullscreen.</value> <value>When in-game, you'll be able to see where your server is located via [ipinfo.io]({0}).</value>
</data> </data>
<data name="Menu.Integrations.ShowServerDetails.Title" xml:space="preserve"> <data name="Menu.Integrations.QueryServerLocation.Title" xml:space="preserve">
<value>See server location when joining a game</value> <value>Query server location</value>
</data> </data>
<data name="Menu.Integrations.Title" xml:space="preserve"> <data name="Menu.Integrations.Title" xml:space="preserve">
<value>Integrations</value> <value>Integrations</value>
@ -999,7 +975,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Installer.Welcome.MainText" xml:space="preserve"> <data name="Installer.Welcome.MainText" xml:space="preserve">
<value>Thank you for downloading Bloxstrap. <value>Thank you for downloading Bloxstrap.
You should have gotten it from either {0} or {1}. Those are the only official websites to get it from. You should have downloaded it from either {0} or {1}. Those are the only official websites to get it from. It is your responsibility to ensure you download from an official source.
This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.</value> This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.</value>
</data> </data>
@ -1120,9 +1096,7 @@ If not, then please report this exception to the maintainers of this fork. Do NO
<value>Connected to reserved server</value> <value>Connected to reserved server</value>
</data> </data>
<data name="Menu.Mods.Presets.EmojiType.Error" xml:space="preserve"> <data name="Menu.Mods.Presets.EmojiType.Error" xml:space="preserve">
<value>The emoji mod could not be applied because of a network error during download. <value>The emoji mod can not be applied at this time.</value>
{0}</value>
</data> </data>
<data name="Dialog.AlreadyRunning.Installer" xml:space="preserve"> <data name="Dialog.AlreadyRunning.Installer" xml:space="preserve">
<value>Please wait for installation to finish.</value> <value>Please wait for installation to finish.</value>
@ -1130,4 +1104,103 @@ If not, then please report this exception to the maintainers of this fork. Do NO
<data name="Dialog.AlreadyRunning.Uninstaller" xml:space="preserve"> <data name="Dialog.AlreadyRunning.Uninstaller" xml:space="preserve">
<value>Please wait for uninstallation to finish.</value> <value>Please wait for uninstallation to finish.</value>
</data> </data>
<data name="About.Title" xml:space="preserve">
<value>About Bloxstrap</value>
</data>
<data name="About.Licenses.Title" xml:space="preserve">
<value>Licenses</value>
</data>
<data name="About.Translators.Title" xml:space="preserve">
<value>Translators</value>
</data>
<data name="Menu.UnsavedChanges" xml:space="preserve">
<value>You have unsaved changes. Are you sure you want to close without saving?</value>
</data>
<data name="InstallChecker.VersionLessThanInstalled" xml:space="preserve">
<value>The version of Bloxstrap you've launched is older than the version you currently have installed.
Issues may occur and your settings may be altered. A reinstall is recommended.
Are you sure you want to continue?</value>
</data>
<data name="Dialog.PlayerError.FailedLaunch" xml:space="preserve">
<value>Roblox failed to launch.</value>
</data>
<data name="Dialog.PlayerError.Crash" xml:space="preserve">
<value>Roblox has crashed.</value>
</data>
<data name="Dialog.PlayerError.HelpInformation" xml:space="preserve">
<value>Please read the following help information, which will open in your web browser when you close this dialog.</value>
</data>
<data name="Common.NetworkError" xml:space="preserve">
<value>Could not load data because of a network error.</value>
</data>
<data name="Common.Loading" xml:space="preserve">
<value>Loading, please wait...</value>
</data>
<data name="About.Supporters.Title" xml:space="preserve">
<value>Supporters</value>
</data>
<data name="About.Supporters.Description" xml:space="preserve">
<value>These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!</value>
</data>
<data name="JsonManager.SettingsLoadFailed" xml:space="preserve">
<value>Your Settings could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="JsonManager.FastFlagsLoadFailed" xml:space="preserve">
<value>Your Fast Flags could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="ContextMenu.GameHistory.Title" xml:space="preserve">
<value>Game history</value>
</data>
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
<value>Rejoin</value>
</data>
<data name="ActivityWatcher.RichPresenceLoadFailed" xml:space="preserve">
<value>Your current game will not show on your Discord presence because an error occurred when loading the game information.</value>
</data>
<data name="ContextMenu.GameHistory.Description" xml:space="preserve">
<value>Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them.</value>
</data>
<data name="ActivityWatcher.LocationQueryFailed" xml:space="preserve">
<value>The server location could not be queried. You may be joining games too quickly.</value>
</data>
<data name="Dialog.Connectivity.ServiceDown" xml:space="preserve">
<value>{0} may be down right now.</value>
</data>
<data name="Dialog.Connectivity.TryAgainLater" xml:space="preserve">
<value>Please try again later.</value>
</data>
<data name="Dialog.Connectivity.RobloxUpgradeSkip" xml:space="preserve">
<value>For this launch, Roblox will not be checked for upgrades, and changes to mods will not be applied.</value>
</data>
<data name="Dialog.Connectivity.RobloxUpgradeNeeded" xml:space="preserve">
<value>Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue.</value>
</data>
<data name="Installer.Install.CannotOverwrite" xml:space="preserve">
<value>Bloxstrap has been installed to this location before and is still present, however the installer cannot overwrite the old executable.
Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards.</value>
</data>
<data name="Common.NotAvailable" xml:space="preserve">
<value>Not available</value>
</data>
<data name="Menu.Mods.Misc.CompatibilitySettings.Title" xml:space="preserve">
<value>Manage compatibility settings</value>
</data>
<data name="Menu.Mods.Misc.CompatibilitySettings.Description" xml:space="preserve">
<value>Configure application parameters such as DPI scaling behaviour and [fullscreen optimizations]({0}).</value>
</data>
<data name="Common.RobloxNotInstalled" xml:space="preserve">
<value>Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option.</value>
</data>
<data name="Installer.ShouldCancel" xml:space="preserve">
<value>Are you sure you want to cancel the installation?</value>
</data>
<data name="Bootstrapper.JsonManagerSaveFailed" xml:space="preserve">
<value>Failed to save {0}: {1}</value>
</data>
<data name="Dialog.IconLoadFailed" xml:space="preserve">
<value>The chosen bootstrapper icon could not be loaded.
{0}</value>
</data>
</root> </root>

View File

@ -15,6 +15,7 @@
private static readonly Dictionary<string, int> BaseUrls = new() private static readonly Dictionary<string, int> BaseUrls = new()
{ {
{ "https://setup.rbxcdn.com", 0 }, { "https://setup.rbxcdn.com", 0 },
{ "https://setup-aws.rbxcdn.com", 2 },
{ "https://setup-ak.rbxcdn.com", 2 }, { "https://setup-ak.rbxcdn.com", 2 },
{ "https://roblox-setup.cachefly.net", 2 }, { "https://roblox-setup.cachefly.net", 2 },
{ "https://s3.amazonaws.com/setup.roblox.com", 4 } { "https://s3.amazonaws.com/setup.roblox.com", 4 }
@ -22,7 +23,7 @@
private static async Task<string?> TestConnection(string url, int priority, CancellationToken token) private static async Task<string?> TestConnection(string url, int priority, CancellationToken token)
{ {
string LOG_IDENT = $"RobloxDeployment::TestConnection.{url}"; string LOG_IDENT = $"RobloxDeployment::TestConnection<{url}>";
await Task.Delay(priority * 1000, token); await Task.Delay(priority * 1000, token);
@ -32,14 +33,14 @@
{ {
var response = await App.HttpClient.GetAsync($"{url}/versionStudio", token); var response = await App.HttpClient.GetAsync($"{url}/versionStudio", token);
if (!response.IsSuccessStatusCode) response.EnsureSuccessStatusCode();
throw new HttpResponseException(response);
// versionStudio is the version hash for the last MFC studio to be deployed. // versionStudio is the version hash for the last MFC studio to be deployed.
// the response body should always be "version-012732894899482c". // the response body should always be "version-012732894899482c".
string content = await response.Content.ReadAsStringAsync(token); string content = await response.Content.ReadAsStringAsync(token);
if (content != VersionStudioHash) if (content != VersionStudioHash)
throw new Exception($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")"); throw new InvalidHTTPResponseException($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")");
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
@ -66,11 +67,10 @@
// returns null for success // returns null for success
CancellationTokenSource tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
var exceptions = new List<Exception>(); var exceptions = new List<Exception>();
var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, token)).ToList(); var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, tokenSource.Token)).ToList();
App.Logger.WriteLine(LOG_IDENT, "Testing connectivity..."); App.Logger.WriteLine(LOG_IDENT, "Testing connectivity...");
@ -127,7 +127,11 @@
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel}"); App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel}");
if (String.IsNullOrEmpty(channel))
channel = DefaultChannel;
string cacheKey = $"{channel}-{binaryType}"; string cacheKey = $"{channel}-{binaryType}";
ClientVersion clientVersion; ClientVersion clientVersion;
if (ClientVersionCache.ContainsKey(cacheKey)) if (ClientVersionCache.ContainsKey(cacheKey))
@ -137,48 +141,27 @@
} }
else else
{ {
bool isDefaultChannel = String.Compare(channel, DefaultChannel, StringComparison.OrdinalIgnoreCase) == 0;
string path = $"/v2/client-version/{binaryType}"; string path = $"/v2/client-version/{binaryType}";
if (String.Compare(channel, DefaultChannel, StringComparison.InvariantCultureIgnoreCase) != 0) if (!isDefaultChannel)
path = $"/v2/client-version/{binaryType}/channel/{channel}"; path = $"/v2/client-version/{binaryType}/channel/{channel}";
HttpResponseMessage deployInfoResponse;
try try
{ {
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path); clientVersion = await Http.GetJson<ClientVersion>("https://clientsettingscdn.roblox.com" + path);
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings..."); App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettings.roblox.com" + path); clientVersion = await Http.GetJson<ClientVersion>("https://clientsettings.roblox.com" + path);
}
string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync();
if (!deployInfoResponse.IsSuccessStatusCode)
{
// 400 = Invalid binaryType.
// 404 = Could not find version details for binaryType.
// 500 = Error while fetching version information.
// either way, we throw
App.Logger.WriteLine(LOG_IDENT,
"Failed to fetch deploy info!\r\n" +
$"\tStatus code: {deployInfoResponse.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseException(deployInfoResponse);
}
clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawResponse)!;
} }
// check if channel is behind LIVE // check if channel is behind LIVE
if (channel != DefaultChannel) if (!isDefaultChannel)
{ {
var defaultClientVersion = await GetInfo(DefaultChannel); var defaultClientVersion = await GetInfo(DefaultChannel);
@ -187,6 +170,7 @@
} }
ClientVersionCache[cacheKey] = clientVersion; ClientVersionCache[cacheKey] = clientVersion;
}
return clientVersion; return clientVersion;
} }

View File

@ -52,16 +52,7 @@ namespace Bloxstrap
string rawResponse = await response.Content.ReadAsStringAsync(); string rawResponse = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) response.EnsureSuccessStatusCode();
{
App.Logger.WriteLine(logIndent,
"Failed to fetch client settings!\r\n" +
$"\tStatus code: {response.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseException(response);
}
var clientSettings = JsonSerializer.Deserialize<ClientFlagSettings>(rawResponse); var clientSettings = JsonSerializer.Deserialize<ClientFlagSettings>(rawResponse);

View File

@ -32,11 +32,11 @@ namespace Bloxstrap.UI.Converters
return attribute.StaticName; return attribute.StaticName;
if (attribute.FromTranslation is not null) if (attribute.FromTranslation is not null)
return Resources.Strings.ResourceManager.GetStringSafe(attribute.FromTranslation); return Strings.ResourceManager.GetStringSafe(attribute.FromTranslation);
} }
} }
return Resources.Strings.ResourceManager.GetStringSafe(String.Format( return Strings.ResourceManager.GetStringSafe(String.Format(
"{0}.{1}", "{0}.{1}",
typeName.Substring(typeName.IndexOf('.', StringComparison.Ordinal) + 1), typeName.Substring(typeName.IndexOf('.', StringComparison.Ordinal) + 1),
stringVal stringVal

View File

@ -0,0 +1,48 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.About.MainWindow"
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:pages="clr-namespace:Bloxstrap.UI.Elements.About.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
Title="{x:Static resources:Strings.About_Title}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
MinWidth="740"
Width="740"
Height="440"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar Padding="8" x:Name="RootTitleBar" Grid.Row="0" ForceShutdown="False" MinimizeToTray="False" UseSnapLayout="True" Title="{x:Static resources:Strings.About_Title}" Icon="pack://application:,,,/Bloxstrap.ico" />
<Grid x:Name="RootGrid" Grid.Row="1" Margin="12,12,0,0" Visibility="Visible">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:NavigationStore x:Name="RootNavigation" Grid.Row="1" Grid.Column="0" Margin="0,0,12,0" Frame="{Binding ElementName=RootFrame}" SelectedPageIndex="0">
<ui:NavigationStore.Items>
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_About_Title}" PageType="{x:Type pages:AboutPage}" Icon="QuestionCircle48" Tag="about" Margin="0,0,0,12" />
<ui:NavigationItem Content="{x:Static resources:Strings.About_Translators_Title}" PageType="{x:Type pages:TranslatorsPage}" Icon="Translate24" Tag="translators" Margin="0,0,0,12" />
<ui:NavigationItem Content="{x:Static resources:Strings.About_Licenses_Title}" PageType="{x:Type pages:LicensesPage}" Icon="Code24" Tag="licenses" Margin="0,0,0,12" />
</ui:NavigationStore.Items>
</ui:NavigationStore>
<Frame x:Name="RootFrame" Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" />
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,35 @@
using System.Windows.Controls;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
namespace Bloxstrap.UI.Elements.About
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : INavigationWindow
{
public MainWindow()
{
InitializeComponent();
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu");
}
#region INavigationWindow methods
public Frame GetFrame() => RootFrame;
public INavigation GetNavigation() => RootNavigation;
public bool Navigate(Type pageType) => RootNavigation.Navigate(pageType);
public void SetPageService(IPageService pageService) => RootNavigation.PageService = pageService;
public void ShowWindow() => Show();
public void CloseWindow() => Close();
#endregion INavigationWindow methods
}
}

View File

@ -0,0 +1,222 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.About.Pages.AboutPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.About"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<!--d:DataContext="{d:DesignInstance dmodels:AboutViewModel, IsDesignTimeCreatable=True}"-->
<StackPanel Margin="0,0,14,14">
<StackPanel.Resources>
<FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
</StackPanel.Resources>
<Grid Margin="0,0,0,24" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="72" Height="72" VerticalAlignment="Center" Source="pack://application:,,,/Bloxstrap.ico" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Bloxstrap" Margin="0,0,4,0" FontSize="24" FontWeight="Medium" />
<TextBlock Grid.Column="1" Text="{Binding Version, Mode=OneTime}" Margin="4,0,0,2" VerticalAlignment="Bottom" FontSize="16" FontWeight="Medium" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</Grid>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
<WrapPanel HorizontalAlignment="Center" Orientation="Horizontal">
<ui:Anchor Margin="0,0,8,8" Content="{x:Static resources:Strings.Menu_About_GithubRepository}" Icon="Code24" NavigateUri="https://github.com/pizzaboxer/bloxstrap" />
<ui:Anchor Margin="0,0,8,8" Content="{x:Static resources:Strings.Menu_About_HelpInformation}" Icon="BookQuestionMark24" NavigateUri="https://github.com/pizzaboxer/bloxstrap/wiki" />
<ui:Anchor Margin="0,0,8,8" Content="{x:Static resources:Strings.Menu_About_ReportIssue}" Icon="BookExclamationMark24" NavigateUri="https://github.com/pizzaboxer/bloxstrap/issues" />
<ui:Anchor Margin="0,0,0,8" Content="{x:Static resources:Strings.Menu_About_DiscordServer}" Icon="Chat48" NavigateUri="https://discord.gg/nKjV3mGq6R" />
</WrapPanel>
<StackPanel Visibility="{Binding BuildInformationVisibility, Mode=OneTime}">
<TextBlock Text="Build Information" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="hmmmm" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Column="0" Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Timestamp" />
<TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildTimestamp, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Machine" />
<TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.Machine, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Hash" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="2" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}">
<Hyperlink Foreground="{DynamicResource TextFillColorTertiaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="{Binding BuildCommitHashUrl, Mode=OneTime}">
<TextBlock Text="{Binding BuildMetadata.CommitHash, Mode=OneTime}" />
</Hyperlink>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Ref" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.CommitRef, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
</Grid>
</StackPanel>
<TextBlock Text="{x:Static resources:Strings.About_Supporters_Title}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<controls:MarkdownTextBlock MarkdownText="{Binding Source={x:Static resources:Strings.About_Supporters_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://ko-fi.com/boxerpizza'}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Unknown}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:ProgressRing Grid.Column="0" IsIndeterminate="True" />
<TextBlock Grid.Column="1" Margin="16,0,0,0" Text="{x:Static resources:Strings.Common_Loading}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Failed}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="pack://application:,,,/Resources/MessageBox/Error.png" Width="60" Height="60" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{x:Static resources:Strings.Common_NetworkError}" />
<TextBlock Text="{Binding SupportersLoadError, Mode=OneWay}" />
</StackPanel>
</Grid>
<ListView ItemsSource="{Binding Supporters, Mode=OneWay}" Margin="0,8,0,0" ScrollViewer.CanContentScroll="False" IsEnabled="False">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Successful}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Height="32" Width="32" VerticalAlignment="Center">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding Image, IsAsync=True}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Margin="8,0,2,0" VerticalAlignment="Center" Text="{Binding Name}" />
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding SupporterColumns}" Margin="-4" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:Expander Grid.Column="0" Margin="0,0,4,0" HeaderIcon="Code24" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_Code}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[Matt](https://github.com/bluepilledgreat)" />
<controls:MarkdownTextBlock MarkdownText="[lolmanurfunny](https://github.com/lolmanurfunny)" />
<controls:MarkdownTextBlock MarkdownText="[1011025m](https://github.com/1011025m)" />
<controls:MarkdownTextBlock MarkdownText="[EasternBloxxer](https://github.com/EasternBloxxer)" />
<controls:MarkdownTextBlock MarkdownText="[sitiom](https://github.com/sitiom)" />
<controls:MarkdownTextBlock MarkdownText="[Extravi](https://github.com/Extravi)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[swatTurret](https://github.com/swatTurret)" />
<controls:MarkdownTextBlock MarkdownText="[fxeP1](https://github.com/fxeP1)" />
<controls:MarkdownTextBlock MarkdownText="[Redusofficial](https://github.com/Redusofficial)" />
<controls:MarkdownTextBlock MarkdownText="[srthMD](https://github.com/srthMD)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Column="1" Margin="4,0,4,0" HeaderIcon="AppsAddIn28" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_FeatureIdeas}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[he3als](https://github.com/he3als)" />
<controls:MarkdownTextBlock MarkdownText="[NikSavchenk0](https://github.com/NikSavchenk0)" />
<controls:MarkdownTextBlock MarkdownText="[carter0nline](https://github.com/carter0nline)" />
<controls:MarkdownTextBlock MarkdownText="[lolmanurfunny](https://github.com/lolmanurfunny)" />
<controls:MarkdownTextBlock MarkdownText="[MehKako](https://github.com/MehKako)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[knivesofeylis](https://github.com/knivesofeylis)" />
<controls:MarkdownTextBlock MarkdownText="[sha4owz](https://github.com/sha4owz)" />
<controls:MarkdownTextBlock MarkdownText="[DaMlgNoodle](https://github.com/DaMlgNoodle)" />
<controls:MarkdownTextBlock MarkdownText="[nakoyasha](https://github.com/nakoyasha)" />
<controls:MarkdownTextBlock MarkdownText="[exurd](https://github.com/exurd)" />
<controls:MarkdownTextBlock MarkdownText="[0xFE0F](https://github.com/0xFE0F)" />
<controls:MarkdownTextBlock MarkdownText="[Tezos](https://github.com/GoingCrazyDude)" />
<controls:MarkdownTextBlock MarkdownText="[CfwSky](https://www.roblox.com/users/129425241/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ruubloo](https://www.roblox.com/users/158082266/profile)" />
<controls:MarkdownTextBlock MarkdownText="[toyoda165](https://www.roblox.com/users/923416649/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ShadowCodeX](https://github.com/ShadowCodeX-debug)" />
<controls:MarkdownTextBlock MarkdownText="[cub-has-injected](https://github.com/cub-has-injected)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Column="2" Margin="4,0,0,0" HeaderIcon="Heart16" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_SpecialThanks}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[MaximumADHD](https://github.com/MaximumADHD)" />
<controls:MarkdownTextBlock MarkdownText="[Multako](https://www.roblox.com/users/2485612194/profile)" />
<controls:MarkdownTextBlock MarkdownText="[axstin](https://github.com/axstin)" />
<controls:MarkdownTextBlock MarkdownText="[taskmanager](https://github.com/Mantaraix)" />
<controls:MarkdownTextBlock MarkdownText="[apprehensions](https://github.com/apprehensions)" />
</StackPanel>
</controls:Expander>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -1,6 +1,6 @@
using Bloxstrap.UI.ViewModels.Settings; using Bloxstrap.UI.ViewModels.About;
namespace Bloxstrap.UI.Elements.Settings.Pages namespace Bloxstrap.UI.Elements.About.Pages
{ {
/// <summary> /// <summary>
/// Interaction logic for AboutPage.xaml /// Interaction logic for AboutPage.xaml

View File

@ -0,0 +1,72 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.About.Pages.LicensesPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<TextBlock Text="{x:Static resources:Strings.Menu_About_Licenses}" FontWeight="Medium" FontSize="24" Margin="0,0,0,16" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Grid.Row="0" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="Bloxstrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/lepoco/wpfui/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="WPF-UI" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/securifybv/ShellLink/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="14" Text="ShellLink" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="DiscordRPC" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="13" Text="SharpZipLib" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<StackPanel>
<TextBlock FontSize="14" Text="Markdig" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.UI.Elements.About.Pages
{
/// <summary>
/// Interaction logic for LicensesPage.xaml
/// </summary>
public partial class LicensesPage
{
public LicensesPage()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,484 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.About.Pages.TranslatorsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<TextBlock Text="{x:Static resources:Strings.About_Translators_Title}" FontWeight="Medium" FontSize="24" Margin="0,0,0,16" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Bahasa Indonesia" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="e7leopard" />
<TextBlock Text="hfzrk" />
<TextBlock Text="soudblox" />
<TextBlock Text="Bokmål" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="darkevilmage" />
<TextBlock Text="endsouls" />
<TextBlock Text="letoek" />
<!--<TextBlock Text="Čeština" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Chusaak" />
<TextBlock Text="DanyCraftCZ" />
<TextBlock Text="Franklin_Surten" />
<TextBlock Text="Galaxy_Gangster6" />
<TextBlock Text="jasperholo" />
<TextBlock Text="letoek" />
<TextBlock Text="noobkid4545" />
<TextBlock Text="radim776" />-->
<!-- <TextBlock Text="Dansk" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Momslayer98" />
<TextBlock Text="SirBlue" /> -->
<TextBlock Text="Deutsch" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="agent_phoenix" />
<TextBlock Text="avoidr" />
<TextBlock Text="hcjohnsgd" />
<TextBlock Text="hxmbt" />
<TextBlock Text="kale123" />
<TextBlock Text="Marvin_Chu" />
<TextBlock Text="nzxt_xll" />
<TextBlock Text="Nlx095" />
<TextBlock Text="Ph1lwtf" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="TEAM_LILA" />
<TextBlock Text="xDevoidx" />
<TextBlock Text="Español" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="4Xisty" />
<TextBlock Text="Botkid" />
<TextBlock Text="colocky" />
<TextBlock Text="D0N-B0T" />
<TextBlock Text="Dasp" />
<TextBlock Text="devyyxn" />
<TextBlock Text="ItzzExcel" />
<TextBlock Text="Ilayhlinda" />
<TextBlock Text="Ilushiouss" />
<TextBlock Text="jayces." />
<TextBlock Text="kroesufos" />
<TextBlock Text="LaiyLiod" />
<TextBlock Text="lyalekin" />
<TextBlock Text="NezumiDS" />
<TextBlock Text="NimuruDP" />
<TextBlock Text="NescafeCL" />
<TextBlock Text="Sw7gger" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="Urzy" />
<TextBlock Text="Filipino" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alphjectiom" />
<TextBlock Text="FlaminDaPotato" />
<TextBlock Text="RobiTheRobloxxer" />
<TextBlock Text="shadow01148" />
<TextBlock Text="Français" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="At0zDx" />
<TextBlock Text="built4aiming" />
<TextBlock Text="hahaloserz360" />
<TextBlock Text="K0ga" />
<TextBlock Text="Marcssebaa" />
<TextBlock Text="MommySernox" />
<TextBlock Text="owentempest8" />
<TextBlock Text="Subsical" />
<TextBlock Text="thatsirwaffles" />
<TextBlock Text="tyundrai" />
<TextBlock Text="Waza80" />
<TextBlock Text="Hindi (Latin)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="kunaljainop" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="Sur_" />
<TextBlock Text="Tezos" />
<TextBlock Text="TheTakuo" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Hrvatski" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Dzigos" />
<TextBlock Text="Koyroii" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Italiano" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="crow_zxcr" />
<TextBlock Text="devyyxn" />
<TextBlock Text="domenicoiacono" />
<TextBlock Text="hulabulaseop" />
<TextBlock Text="lord_moth" />
<TextBlock Text="loridori" />
<TextBlock Text="Lupo01" />
<TextBlock Text="Mogunars" />
<TextBlock Text="pave08" />
<TextBlock Text="spectrumbruh" />
<TextBlock Text="Lietuvių" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="cr0000142" />
<TextBlock Text="Duexo" />
<TextBlock Text="jessethedev" />
<TextBlock Text="Vac31." />
<TextBlock Text="Magyar" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="DynoPlays" />
<TextBlock Text="Elotomka" />
<TextBlock Text="xM4rk1" />
<!--<TextBlock Text="Nederlands" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cosmix" />
<TextBlock Text="Miwwzy" />
<TextBlock Text="Quickvision1" />
<TextBlock Text="ydboss" />-->
<TextBlock Text="Polski" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Eztaru" />
<TextBlock Text="lunar" />
<TextBlock Text="markontm" />
<TextBlock Text="my5q" />
<TextBlock Text="nemzik2137" />
<TextBlock Text="plexar" />
<TextBlock Text="r.efil" />
<TextBlock Text="Português (Brasil)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="anormalevis" />
<TextBlock Text="ErisvaldoBalbino" />
<TextBlock Text="G3xneric" />
<TextBlock Text="hnter" />
<TextBlock Text="issei_" />
<TextBlock Text="iyto.lk" />
<TextBlock Text="jhermesn" />
<TextBlock Text="JorgeDaPelada" />
<TextBlock Text="LwgoDev" />
<TextBlock Text="nunk7" />
<TextBlock Text="peke7374" />
<TextBlock Text="SeeF" />
<TextBlock Text="Snowzin1" />
<TextBlock Text="storm930" />
<TextBlock Text="toofastforboo" />
<TextBlock Text="VMOICE" />
<TextBlock Text="Ye4" />
<TextBlock Text="ZaPeZaPe" />
<TextBlock Text="Română" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Externalkinetics" />
<TextBlock Text="MonochromeAlex" />
<TextBlock Text="PlayerValley" />
<TextBlock Text="Smuki" />
<TextBlock Text="theflopperguy" />
<TextBlock Text="Suomi" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="gelaxiz" />
<TextBlock Text="jes5" />
<TextBlock Text="retromaxwell" />
<TextBlock Text="SomePinglord" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Svenska" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Axellse" />
<TextBlock Text="CroppingFlea479" />
<TextBlock Text="FishySpelar" />
<TextBlock Text="PineappleSnackz" />
<TextBlock Text="simonixen" />
<TextBlock Text="thatgurkangurk" />
<TextBlock Text="Tiếng Việt" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alexking2068" />
<TextBlock Text="baterblx" />
<TextBlock Text="Elytronn" />
<TextBlock Text="fox810891" />
<TextBlock Text="ItsPoofy" />
<TextBlock Text="Limer1" />
<TextBlock Text="makayu203332" />
<TextBlock Text="MEx2/j7x6" />
<TextBlock Text="NguyenDat208" />
<TextBlock Text="quanmequankk" />
<TextBlock Text="SomeRandomGuy175" />
<TextBlock Text="SonThanhVN" />
<TextBlock Text="teaanguyenn" />
<TextBlock Text="Veiiorra" />
<TextBlock Text="Türkçe" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="canny19133" />
<TextBlock Text="cfors55" />
<TextBlock Text="drakfreddy" />
<TextBlock Text="enisify" />
<TextBlock Text="jayces." />
<TextBlock Text="nyatie" />
<TextBlock Text="PixelArmy" />
<TextBlock Text="plants8332" />
<TextBlock Text="r02" />
<TextBlock Text="siyamicik" />
<TextBlock Text="ydboss" />
<TextBlock Text="Українська" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="9zh" />
<TextBlock Text="andrey3569s" />
<TextBlock Text="DexterBloxxer" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="maksimvlad7" />
<TextBlock Text="Босански" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cortex_1" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Ren" />
<TextBlock Text="Български" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="GrafitNiki" />
<TextBlock Text="sidefrappe" />
<TextBlock Text="Русский" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="3tcy" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="andBroz" />
<TextBlock Text="alexneonwithglue" />
<TextBlock Text="AnonymousDudeLOL123" />
<TextBlock Text="aperna_of_the_ticks" />
<TextBlock Text="arsenijveselov77" />
<TextBlock Text="Art3mLapa" />
<TextBlock Text="cherkash" />
<TextBlock Text="cub-has-injected" />
<TextBlock Text="dallyuser" />
<TextBlock Text="Dr1mG" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="fxstyxx" />
<TextBlock Text="Gustodd4202" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="harababura" />
<TextBlock Text="ImperialRhyme" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="IStoleYourCheese" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="khat7" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="kostyan" />
<TextBlock Text="Maks" />
<TextBlock Text="niktoyou" />
<TextBlock Text="nurgament2" />
<TextBlock Text="poflexim" />
<TextBlock Text="Prob1rka" />
<TextBlock Text="Provo" />
<TextBlock Text="Quenevelly" />
<TextBlock Text="sally13249" />
<TextBlock Text="simmon8800" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Skylan031" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Spuffio" />
<TextBlock Text="StraiF" />
<TextBlock Text="StrayCatSimb" />
<TextBlock Text="Voxel" />
<TextBlock Text="XonaShera" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Ziio123" />
<TextBlock Text="zor9na90000" />
</StackPanel>
<StackPanel Grid.Column="3">
<TextBlock Text="עברית" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="1cur1" />
<TextBlock Text="ilan0098" />
<TextBlock Text="koerga" />
<TextBlock Text="Sezei" />
<TextBlock Text="العربية" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="busguesjshbahsj" />
<TextBlock Text="cq2i" />
<TextBlock Text="mmiky" />
<TextBlock Text="mostafagamingx1" />
<TextBlock Text="RoRed" />
<TextBlock Text="Sakupen" />
<TextBlock Text="streoic" />
<TextBlock Text="uvq18" />
<TextBlock Text="wyfast" />
<TextBlock Text="Zida" />
<TextBlock Text="বাংলা" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Arnian" />
<TextBlock Text="Hydrated_panda" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="red_hi" />
<TextBlock Text="ภาษาไทย" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="._bonus_." />
<TextBlock Text="marc15772" />
<TextBlock Text="arthurwagon" />
<TextBlock Text="Sem1z" />
<TextBlock Text="xAom" />
<TextBlock Text="한국어" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="ADVI50R" />
<TextBlock Text="asd123456fghqwerty" />
<TextBlock Text="bacon1295" />
<TextBlock Text="NightPlay" />
<TextBlock Text="中文 (简体)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="Aling00" />
<TextBlock Text="Clock" />
<TextBlock Text="ERSN_CERROR" />
<TextBlock Text="Kirxvil" />
<TextBlock Text="Typel" />
<TextBlock Text="yuhaodatt" />
<TextBlock Text="中文 (廣東話)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="henrychu1125" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="shhh_op" />
<TextBlock Text="中文 (繁體)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="DXuwu" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="日本語" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="MintJapan" />
<TextBlock Text="pimeja7" />
<TextBlock Text="yixhuaa" />
</StackPanel>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.UI.Elements.About.Pages
{
/// <summary>
/// Interaction logic for TranslatorsPage.xaml
/// </summary>
public partial class TranslatorsPage
{
public TranslatorsPage()
{
InitializeComponent();
}
}
}

View File

@ -133,7 +133,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base
public void Dialog_FormClosing(object sender, FormClosingEventArgs e) public void Dialog_FormClosing(object sender, FormClosingEventArgs e)
{ {
if (!_isClosing) if (!_isClosing)
Bootstrapper?.CancelInstall(); Bootstrapper?.Cancel();
} }
#endregion #endregion

View File

@ -126,7 +126,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private void Window_Closing(object sender, CancelEventArgs e) private void Window_Closing(object sender, CancelEventArgs e)
{ {
if (!_isClosing) if (!_isClosing)
Bootstrapper?.CancelInstall(); Bootstrapper?.Cancel();
} }
#region IBootstrapperDialog Methods #region IBootstrapperDialog Methods

View File

@ -105,7 +105,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private void UiWindow_Closing(object sender, CancelEventArgs e) private void UiWindow_Closing(object sender, CancelEventArgs e)
{ {
if (!_isClosing) if (!_isClosing)
Bootstrapper?.CancelInstall(); Bootstrapper?.Cancel();
} }
#region IBootstrapperDialog Methods #region IBootstrapperDialog Methods

View File

@ -123,7 +123,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private void UiWindow_Closing(object sender, CancelEventArgs e) private void UiWindow_Closing(object sender, CancelEventArgs e)
{ {
if (!_isClosing) if (!_isClosing)
Bootstrapper?.CancelInstall(); Bootstrapper?.Cancel();
} }
#region IBootstrapperDialog Methods #region IBootstrapperDialog Methods

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
InitializeComponent(); InitializeComponent();
this.buttonCancel.Text = Resources.Strings.Common_Cancel; this.buttonCancel.Text = Strings.Common_Cancel;
ScaleWindow(); ScaleWindow();
SetupDialog(); SetupDialog();

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
InitializeComponent(); InitializeComponent();
this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().ToBitmap(); this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().ToBitmap();
this.buttonCancel.Text = Resources.Strings.Common_Cancel; this.buttonCancel.Text = Strings.Common_Cancel;
ScaleWindow(); ScaleWindow();
SetupDialog(); SetupDialog();

View File

@ -52,8 +52,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
this.BackColor = Color.FromArgb(25, 27, 29); this.BackColor = Color.FromArgb(25, 27, 29);
} }
this.labelMessage.Text = Resources.Strings.Bootstrapper_StylePreview_TextCancel; this.labelMessage.Text = Strings.Bootstrapper_StylePreview_TextCancel;
this.buttonCancel.Text = Resources.Strings.Common_Cancel; this.buttonCancel.Text = Strings.Common_Cancel;
this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().GetSized(128, 128).ToBitmap(); this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().GetSized(128, 128).ToBitmap();
SetupDialog(); SetupDialog();

View File

@ -57,11 +57,23 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="Info28"/> <ui:SymbolIcon Grid.Column="0" Symbol="Info28"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_SeeServerDetails}" /> <TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Title}" />
</Grid> </Grid>
</MenuItem.Header> </MenuItem.Header>
</MenuItem> </MenuItem>
<MenuItem x:Name="CloseRobloxMenuItem" Visibility="Collapsed" Click="CloseRobloxMenuItem_Click"> <MenuItem x:Name="GameHistoryMenuItem" Click="JoinLastServerMenuItem_Click" Visibility="Collapsed">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="History24"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem Click="CloseRobloxMenuItem_Click">
<MenuItem.Header> <MenuItem.Header>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -73,7 +85,7 @@
</Grid> </Grid>
</MenuItem.Header> </MenuItem.Header>
</MenuItem> </MenuItem>
<MenuItem x:Name="LogTracerMenuItem" Click="LogTracerMenuItem_Click"> <MenuItem x:Name="LogTracerMenuItem" Visibility="Collapsed" Click="LogTracerMenuItem_Click">
<MenuItem.Header> <MenuItem.Header>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -81,7 +93,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="DocumentCatchUp20"/> <ui:SymbolIcon Grid.Column="0" Symbol="DocumentCatchUp20"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_OpenLogFile}" /> <TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.Common_OpenLogFile}" />
</Grid> </Grid>
</MenuItem.Header> </MenuItem.Header>
</MenuItem> </MenuItem>

View File

@ -2,16 +2,11 @@
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop; using System.Windows.Interop;
using Wpf.Ui.Appearance;
using Wpf.Ui.Mvvm.Contracts;
using Wpf.Ui.Mvvm.Services;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
using Bloxstrap.Integrations; using Bloxstrap.Integrations;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.Elements.ContextMenu namespace Bloxstrap.UI.Elements.ContextMenu
{ {
@ -22,31 +17,32 @@ 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 // 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 ActivityWatcher? _activityWatcher; private readonly Watcher _watcher;
private readonly DiscordRichPresence? _richPresenceHandler;
private ActivityWatcher? _activityWatcher => _watcher.ActivityWatcher;
private ServerInformation? _serverInformationWindow; private ServerInformation? _serverInformationWindow;
private int? _processId;
public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler, int? processId) private ServerHistory? _gameHistoryWindow;
public MenuContainer(Watcher watcher)
{ {
InitializeComponent(); InitializeComponent();
_activityWatcher = activityWatcher; _watcher = watcher;
_richPresenceHandler = richPresenceHandler;
_processId = processId;
if (_activityWatcher is not null) if (_activityWatcher is not null)
{ {
_activityWatcher.OnLogOpen += ActivityWatcher_OnLogOpen;
_activityWatcher.OnGameJoin += ActivityWatcher_OnGameJoin; _activityWatcher.OnGameJoin += ActivityWatcher_OnGameJoin;
_activityWatcher.OnGameLeave += ActivityWatcher_OnGameLeave; _activityWatcher.OnGameLeave += ActivityWatcher_OnGameLeave;
} }
if (_richPresenceHandler is not null) if (_watcher.RichPresence is not null)
RichPresenceMenuItem.Visibility = Visibility.Visible; RichPresenceMenuItem.Visibility = Visibility.Visible;
if (_processId is not null) if (!App.Settings.Prop.UseDisableAppPatch)
CloseRobloxMenuItem.Visibility = Visibility.Visible; GameHistoryMenuItem.Visibility = Visibility.Visible;
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}"; VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
} }
@ -55,27 +51,33 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{ {
if (_serverInformationWindow is null) if (_serverInformationWindow is null)
{ {
_serverInformationWindow = new ServerInformation(_activityWatcher!); _serverInformationWindow = new(_watcher);
_serverInformationWindow.Closed += (_, _) => _serverInformationWindow = null; _serverInformationWindow.Closed += (_, _) => _serverInformationWindow = null;
} }
if (!_serverInformationWindow.IsVisible) if (!_serverInformationWindow.IsVisible)
_serverInformationWindow.Show(); _serverInformationWindow.ShowDialog();
else
_serverInformationWindow.Activate(); _serverInformationWindow.Activate();
} }
private void ActivityWatcher_OnGameJoin(object? sender, EventArgs e) public void ActivityWatcher_OnLogOpen(object? sender, EventArgs e) =>
Dispatcher.Invoke(() => LogTracerMenuItem.Visibility = Visibility.Visible);
public void ActivityWatcher_OnGameJoin(object? sender, EventArgs e)
{ {
if (_activityWatcher is null)
return;
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
if (_activityWatcher?.ActivityServerType == ServerType.Public) if (_activityWatcher.Data.ServerType == ServerType.Public)
InviteDeeplinkMenuItem.Visibility = Visibility.Visible; InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
ServerDetailsMenuItem.Visibility = Visibility.Visible; ServerDetailsMenuItem.Visibility = Visibility.Visible;
}); });
} }
private void ActivityWatcher_OnGameLeave(object? sender, EventArgs e) public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
{ {
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed; InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
@ -100,9 +102,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu
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); private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _watcher.RichPresence?.SetVisibility(((MenuItem)sender).IsChecked);
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject($"roblox://experiences/start?placeId={_activityWatcher?.ActivityPlaceId}&gameInstanceId={_activityWatcher?.ActivityJobId}"); private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject(_activityWatcher?.Data.GetInviteDeeplink());
private void ServerDetailsMenuItem_Click(object sender, RoutedEventArgs e) => ShowServerInformationWindow(); private void ServerDetailsMenuItem_Click(object sender, RoutedEventArgs e) => ShowServerInformationWindow();
@ -110,12 +112,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{ {
string? location = _activityWatcher?.LogLocation; string? location = _activityWatcher?.LogLocation;
if (location is null) if (location is not null)
{
Frontend.ShowMessageBox(Strings.ContextMenu_RobloxNotRunning, MessageBoxImage.Information);
return;
}
Utilities.ShellExecute(location); Utilities.ShellExecute(location);
} }
@ -130,9 +127,24 @@ namespace Bloxstrap.UI.Elements.ContextMenu
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
using Process process = Process.GetProcessById((int)_processId!); _watcher.KillRobloxProcess();
process.Kill(); }
process.Close();
private void JoinLastServerMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_activityWatcher is null)
throw new ArgumentNullException(nameof(_activityWatcher));
if (_gameHistoryWindow is null)
{
_gameHistoryWindow = new(_activityWatcher);
_gameHistoryWindow.Closed += (_, _) => _gameHistoryWindow = null;
}
if (!_gameHistoryWindow.IsVisible)
_gameHistoryWindow.ShowDialog();
else
_gameHistoryWindow.Activate();
} }
} }
} }

View File

@ -0,0 +1,107 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.ServerHistory"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
d:DataContext="{d:DesignInstance Type=models:ServerHistoryViewModel}"
mc:Ignorable="d"
Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}"
MinWidth="420"
MinHeight="420"
Width="580"
Height="420"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<TextBlock Grid.Row="1" Margin="16,8,16,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Description}" TextWrapping="Wrap" />
<TextBlock Grid.Row="2" Margin="16,8,16,0" Text="{Binding Error, Mode=OneWay}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Failed}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</TextBlock.Style>
</TextBlock>
<Border Grid.Row="2">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Unknown}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Border.Style>
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
</Border>
<ListView Grid.Row="2" ItemsSource="{Binding GameHistory, Mode=OneWay}" Margin="8">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Successful}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="12">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
<Border.Background>
<ImageBrush ImageSource="{Binding UniverseDetails.Thumbnail.ImageUrl, IsAsync=True}" />
</Border.Background>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{Binding UniverseDetails.Data.Name}" FontSize="18" FontWeight="Medium" />
<TextBlock Text="{Binding GameHistoryDescription}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:Button Margin="0,8,0,0" Content="{x:Static resources:Strings.ContextMenu_GameHistory_Rejoin}" Command="{Binding RejoinServerCommand}"
Appearance="Success" Foreground="White" IconForeground="White" Icon="Play28" IconFilled="True" />
</StackPanel>
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Border Grid.Row="3" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,21 @@
using Bloxstrap.Integrations;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
{
/// <summary>
/// Interaction logic for ServerInformation.xaml
/// </summary>
public partial class ServerHistory
{
public ServerHistory(ActivityWatcher watcher)
{
var viewModel = new ServerHistoryViewModel(watcher);
viewModel.RequestCloseEvent += (_, _) => Close();
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -46,14 +46,14 @@
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_InstanceId}" /> <TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_InstanceId}" />
<TextBlock Grid.Row="1" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding InstanceId, Mode=OneWay}" /> <TextBlock Grid.Row="1" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding InstanceId, Mode=OneWay}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Location}" /> <TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Location}" Visibility="{Binding ServerLocationVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ServerLocation, Mode=OneWay}" /> <TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ServerLocation, Mode=OneWay}" Visibility="{Binding ServerLocationVisibility, Mode=OneTime}" />
</Grid> </Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}"> <Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button MinWidth="100" Content="{x:Static resources:Strings.ContextMenu_ServerInformation_CopyInstanceId}" Command="{Binding CopyInstanceIdCommand, Mode=OneTime}" /> <Button MinWidth="100" Content="{x:Static resources:Strings.ContextMenu_ServerInformation_CopyInstanceId}" Command="{Binding CopyInstanceIdCommand, Mode=OneTime}" />
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Command="{Binding CloseWindowCommand, Mode=OneTime}" /> <Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>

View File

@ -22,9 +22,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu
/// </summary> /// </summary>
public partial class ServerInformation public partial class ServerInformation
{ {
public ServerInformation(ActivityWatcher activityWatcher) public ServerInformation(Watcher watcher)
{ {
DataContext = new ServerInformationViewModel(this, activityWatcher); DataContext = new ServerInformationViewModel(watcher);
InitializeComponent(); InitializeComponent();
} }
} }

View File

@ -12,6 +12,7 @@
Width="480" Width="480"
MinHeight="0" MinHeight="0"
SizeToContent="Height" SizeToContent="Height"
Title="{x:Static resources:Strings.Dialog_Connectivity_Title}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}" Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True" ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
@ -29,9 +30,9 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </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" /> <Image x:Name="IconImage" 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"> <StackPanel Grid.Column="1">
<TextBlock x:Name="TitleTextBlock" Text="? is unable to connect to ?" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" /> <TextBlock x:Name="TitleTextBlock" Text="? is unable to connect to ?" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" TextWrapping="Wrap" />
<controls:MarkdownTextBlock x:Name="DescriptionTextBlock" MarkdownText="?" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" /> <controls:MarkdownTextBlock x:Name="DescriptionTextBlock" MarkdownText="?" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="{x:Static resources:Strings.Dialog_Connectivity_MoreInfo}" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" /> <TextBlock Text="{x:Static resources:Strings.Dialog_Connectivity_MoreInfo}" 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" /> <RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,8,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" />

View File

@ -1,5 +1,7 @@
using System.Media; using System.Media;
using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
@ -14,10 +16,41 @@ namespace Bloxstrap.UI.Elements.Dialogs
/// </summary> /// </summary>
public partial class ConnectivityDialog public partial class ConnectivityDialog
{ {
public ConnectivityDialog(string title, string description, Exception exception) public ConnectivityDialog(string title, string description, MessageBoxImage image, Exception exception)
{ {
InitializeComponent(); InitializeComponent();
string? iconFilename = null;
SystemSound? sound = null;
switch (image)
{
case MessageBoxImage.Error:
iconFilename = "Error";
sound = SystemSounds.Hand;
break;
case MessageBoxImage.Question:
iconFilename = "Question";
sound = SystemSounds.Question;
break;
case MessageBoxImage.Warning:
iconFilename = "Warning";
sound = SystemSounds.Exclamation;
break;
case MessageBoxImage.Information:
iconFilename = "Information";
sound = SystemSounds.Asterisk;
break;
}
if (iconFilename is null)
IconImage.Visibility = Visibility.Collapsed;
else
IconImage.Source = new BitmapImage(new Uri($"pack://application:,,,/Resources/MessageBox/{iconFilename}.png"));
TitleTextBlock.Text = title; TitleTextBlock.Text = title;
DescriptionTextBlock.MarkdownText = description; DescriptionTextBlock.MarkdownText = description;
@ -28,7 +61,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
Close(); Close();
}; };
SystemSounds.Hand.Play(); sound?.Play();
Loaded += delegate Loaded += delegate
{ {

View File

@ -33,7 +33,7 @@
</Grid.ColumnDefinitions> </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" /> <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"> <StackPanel Grid.Column="1">
<TextBlock Text="{x:Static resources:Strings.Dialog_Exception_Info_1}" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" /> <TextBlock Text="{x:Static resources:Strings.Dialog_Exception_Info_1}" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" TextWrapping="Wrap" />
<RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,16,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" /> <RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,16,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" />
<controls:MarkdownTextBlock x:Name="HelpMessageMDTextBlock" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" /> <controls:MarkdownTextBlock x:Name="HelpMessageMDTextBlock" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel> </StackPanel>
@ -41,7 +41,7 @@
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}"> <Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button x:Name="LocateLogFileButton" Content="{x:Static resources:Strings.Common_LocateLogFile}" /> <Button x:Name="LocateLogFileButton" Content="{x:Static resources:Strings.Common_OpenLogFile}" />
<Button x:Name="CloseButton" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Margin="12,0,0,0" /> <Button x:Name="CloseButton" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Margin="12,0,0,0" />
</StackPanel> </StackPanel>
</Border> </Border>

View File

@ -1,13 +1,11 @@
using System.Media; using System.Media;
using System.Web;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop; using System.Windows.Interop;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.Elements.Dialogs namespace Bloxstrap.UI.Elements.Dialogs
{ {
// hmm... do i use MVVM for this? // hmm... do i use MVVM for this?
@ -26,19 +24,29 @@ namespace Bloxstrap.UI.Elements.Dialogs
if (!App.Logger.Initialized) if (!App.Logger.Initialized)
LocateLogFileButton.Content = Strings.Dialog_Exception_CopyLogContents; LocateLogFileButton.Content = Strings.Dialog_Exception_CopyLogContents;
string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, "https://github.com/pizzaboxer/bloxstrap/wiki", "https://github.com/pizzaboxer/bloxstrap/issues/new?template=bug_report.yaml"); string repoUrl = $"https://github.com/{App.ProjectRepository}";
string wikiUrl = $"{repoUrl}/wiki";
if (String.IsNullOrEmpty(App.BuildMetadata.CommitHash)) string issueUrl = String.Format(
helpMessage = String.Format(Strings.Dialog_Exception_Info_2_Alt, "https://github.com/pizzaboxer/bloxstrap/wiki"); "{0}/issues/new?template=bug_report.yaml&title={1}&log={2}",
repoUrl,
HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}"),
HttpUtility.UrlEncode(String.Join('\n', App.Logger.History))
);
string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, wikiUrl, issueUrl);
if (!App.IsActionBuild && !App.BuildMetadata.Machine.Contains("pizzaboxer", StringComparison.Ordinal))
helpMessage = String.Format(Strings.Dialog_Exception_Info_2_Alt, wikiUrl);
HelpMessageMDTextBlock.MarkdownText = helpMessage; HelpMessageMDTextBlock.MarkdownText = helpMessage;
LocateLogFileButton.Click += delegate LocateLogFileButton.Click += delegate
{ {
if (App.Logger.Initialized) if (App.Logger.Initialized && !String.IsNullOrEmpty(App.Logger.FileLocation))
Process.Start("explorer.exe", $"/select,\"{App.Logger.FileLocation}\""); Utilities.ShellExecute(App.Logger.FileLocation);
else else
Clipboard.SetDataObject(String.Join("\r\n", App.Logger.Backlog)); Clipboard.SetDataObject(String.Join("\r\n", App.Logger.History));
}; };
CloseButton.Click += delegate CloseButton.Click += delegate

View File

@ -41,7 +41,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
case MessageBoxImage.Warning: case MessageBoxImage.Warning:
iconFilename = "Warning"; iconFilename = "Warning";
sound = SystemSounds.Asterisk; sound = SystemSounds.Exclamation;
break; break;
case MessageBoxImage.Information: case MessageBoxImage.Information:
@ -121,13 +121,13 @@ namespace Bloxstrap.UI.Elements.Dialogs
switch (result) switch (result)
{ {
case MessageBoxResult.OK: case MessageBoxResult.OK:
return Bloxstrap.Resources.Strings.Common_OK; return Strings.Common_OK;
case MessageBoxResult.Cancel: case MessageBoxResult.Cancel:
return Bloxstrap.Resources.Strings.Common_Cancel; return Strings.Common_Cancel;
case MessageBoxResult.Yes: case MessageBoxResult.Yes:
return Bloxstrap.Resources.Strings.Common_Yes; return Strings.Common_Yes;
case MessageBoxResult.No: case MessageBoxResult.No:
return Bloxstrap.Resources.Strings.Common_No; return Strings.Common_No;
default: default:
Debug.Assert(false); Debug.Assert(false);
return result.ToString(); return result.ToString();

View File

@ -12,7 +12,7 @@
Title="Bloxstrap" Title="Bloxstrap"
MinWidth="0" MinWidth="0"
MinHeight="0" MinHeight="0"
Width="320" Width="580"
SizeToContent="Height" SizeToContent="Height"
ResizeMode="NoResize" ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}" Background="{ui:ThemeResource ApplicationBackgroundBrush}"
@ -24,11 +24,40 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" Title="Bloxstrap" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" /> <ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" ShowMinimize="False" ShowMaximize="False" Title="Bloxstrap" Icon="pack://application:,,,/Bloxstrap.ico" CanMaximize="False" KeyboardNavigation.TabNavigation="None" />
<StackPanel Grid.Row="1" Margin="12"> <Grid Grid.Row="1">
<TextBlock FontSize="24" Text="{x:Static resources:Strings.LaunchMenu_Title}" HorizontalAlignment="Center" Margin="0,0,0,16" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" HorizontalAlignment="Center" Margin="0,0,0,32">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="64" Height="64" Source="pack://application:,,,/Bloxstrap.ico" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Bloxstrap" FontSize="24" />
<TextBlock Text="{Binding Version, Mode=OneTime}" VerticalAlignment="Bottom" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="1" HorizontalAlignment="Center">
<ui:Hyperlink Icon="QuestionCircle48" Content="About Bloxstrap" Margin="0,0,0,8" Command="{Binding LaunchAboutCommand, Mode=OneTime}" />
<ui:Hyperlink Icon="Heart48" Content="Support us on Ko-fi!" NavigateUri="https://ko-fi.com/boxerpizza" />
</StackPanel>
</Grid>
<StackPanel Grid.Column="1" Margin="16">
<ui:CardAction Icon="ArrowRight12" Command="{Binding LaunchRobloxCommand, Mode=OneTime}"> <ui:CardAction Icon="ArrowRight12" Command="{Binding LaunchRobloxCommand, Mode=OneTime}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}" />
@ -41,14 +70,15 @@
</StackPanel> </StackPanel>
</ui:CardAction> </ui:CardAction>
<Border Margin="16" />
<ui:CardAction Margin="0,8,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/"> <ui:CardAction Margin="0,8,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Title}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" /> <TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel> </StackPanel>
</ui:CardAction> </ui:CardAction>
<TextBlock Margin="0,16,0,0" FontSize="12" Text="{Binding Version, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Grid>
</base:WpfUiWindow> </base:WpfUiWindow>

View File

@ -9,6 +9,7 @@ using Bloxstrap.UI.Elements.Installer.Pages;
using Bloxstrap.UI.Elements.Base; using Bloxstrap.UI.Elements.Base;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Reflection.Metadata.Ecma335; using System.Reflection.Metadata.Ecma335;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.Elements.Installer namespace Bloxstrap.UI.Elements.Installer
{ {
@ -102,7 +103,7 @@ namespace Bloxstrap.UI.Elements.Installer
if (Finished) if (Finished)
return; return;
var result = Frontend.ShowMessageBox("Are you sure you want to cancel the installation?", MessageBoxImage.Warning, MessageBoxButton.YesNo); var result = Frontend.ShowMessageBox(Strings.Installer_ShouldCancel, MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
e.Cancel = true; e.Cancel = true;

View File

@ -15,7 +15,8 @@
Background="{ui:ThemeResource ApplicationBackgroundBrush}" Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True" ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica" WindowBackdropType="Mica"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen"
Closing="WpfUiWindow_Closing">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -60,7 +61,7 @@
<ui:NavigationItem Content="" PageType="{x:Type pages:FastFlagEditorWarningPage}" Tag="fastflageditorwarning" Visibility="Collapsed" x:Name="EditorWarningNavItem" /> <ui:NavigationItem Content="" PageType="{x:Type pages:FastFlagEditorWarningPage}" Tag="fastflageditorwarning" Visibility="Collapsed" x:Name="EditorWarningNavItem" />
</ui:NavigationFluent.Items> </ui:NavigationFluent.Items>
<ui:NavigationFluent.Footer> <ui:NavigationFluent.Footer>
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_About_Title}" PageType="{x:Type pages:AboutPage}" Icon="QuestionCircle48" Tag="about" Margin="0,0,0,12" /> <ui:NavigationItem Content="{x:Static resources:Strings.Menu_About_Title}" Icon="QuestionCircle48" Margin="0,0,0,12" Command="{Binding OpenAboutCommand, Mode=OneTime}" />
</ui:NavigationFluent.Footer> </ui:NavigationFluent.Footer>
</ui:NavigationFluent> </ui:NavigationFluent>
@ -93,7 +94,7 @@
<ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" /> <ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" />
</StatusBarItem> </StatusBarItem>
<StatusBarItem Grid.Column="2" Padding="4,0,0,0"> <StatusBarItem Grid.Column="2" Padding="4,0,0,0">
<ui:Button Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" /> <ui:Button Content="{x:Static resources:Strings.Common_Close}" Command="{Binding CloseWindowCommand, Mode=OneWay}" />
</StatusBarItem> </StatusBarItem>
</StatusBar> </StatusBar>
</Grid> </Grid>

View File

@ -1,6 +1,10 @@
using System.Windows.Controls; using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using Wpf.Ui.Controls.Interfaces; using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts; using Wpf.Ui.Mvvm.Contracts;
using Bloxstrap.UI.ViewModels.Settings; using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Settings namespace Bloxstrap.UI.Elements.Settings
@ -10,10 +14,14 @@ namespace Bloxstrap.UI.Elements.Settings
/// </summary> /// </summary>
public partial class MainWindow : INavigationWindow public partial class MainWindow : INavigationWindow
{ {
private Models.Persistable.WindowState _state => App.State.Prop.SettingsWindow;
public MainWindow(bool showAlreadyRunningWarning) public MainWindow(bool showAlreadyRunningWarning)
{ {
var viewModel = new MainWindowViewModel(); var viewModel = new MainWindowViewModel();
viewModel.RequestSaveNoticeEvent += (_, _) => SettingsSavedSnackbar.Show(); viewModel.RequestSaveNoticeEvent += (_, _) => SettingsSavedSnackbar.Show();
viewModel.RequestCloseWindowEvent += (_, _) => Close();
DataContext = viewModel; DataContext = viewModel;
@ -22,11 +30,35 @@ namespace Bloxstrap.UI.Elements.Settings
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu"); App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu");
#if DEBUG // easier access #if DEBUG // easier access
EditorWarningNavItem.Visibility = System.Windows.Visibility.Visible; EditorWarningNavItem.Visibility = Visibility.Visible;
#endif #endif
if (showAlreadyRunningWarning) if (showAlreadyRunningWarning)
ShowAlreadyRunningSnackbar(); ShowAlreadyRunningSnackbar();
LoadState();
}
public void LoadState()
{
if (_state.Left > SystemParameters.VirtualScreenWidth)
_state.Left = 0;
if (_state.Top > SystemParameters.VirtualScreenHeight)
_state.Top = 0;
if (_state.Width > 0)
this.Width = _state.Width;
if (_state.Height > 0)
this.Height = _state.Height;
if (_state.Left > 0 && _state.Top > 0)
{
this.WindowStartupLocation = WindowStartupLocation.Manual;
this.Left = _state.Left;
this.Top = _state.Top;
}
} }
private async void ShowAlreadyRunningSnackbar() private async void ShowAlreadyRunningSnackbar()
@ -50,5 +82,27 @@ namespace Bloxstrap.UI.Elements.Settings
public void CloseWindow() => Close(); public void CloseWindow() => Close();
#endregion INavigationWindow methods #endregion INavigationWindow methods
private void WpfUiWindow_Closing(object sender, CancelEventArgs e)
{
if (App.FastFlags.Changed || App.PendingSettingTasks.Any())
{
var result = Frontend.ShowMessageBox(Strings.Menu_UnsavedChanges, MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
e.Cancel = true;
}
_state.Width = this.Width;
_state.Height = this.Height;
_state.Top = this.Top;
_state.Left = this.Left;
App.State.Save();
if (!e.Cancel)
App.Terminate();
}
} }
} }

View File

@ -1,665 +0,0 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.AboutPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="60" Height="60" VerticalAlignment="Center" Source="pack://application:,,,/Bloxstrap.ico" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Bloxstrap" Margin="0,0,4,0" FontSize="24" FontWeight="Medium" />
<TextBlock Grid.Column="1" Text="{Binding Version, Mode=OneTime}" Margin="4,0,0,2" VerticalAlignment="Bottom" FontSize="16" FontWeight="Medium" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</Grid>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
<WrapPanel Orientation="Horizontal">
<ui:Anchor Margin="0,16,8,0" Content="{x:Static resources:Strings.Menu_About_GithubRepository}" Icon="Code24" NavigateUri="https://github.com/pizzaboxer/bloxstrap" />
<ui:Anchor Margin="0,16,8,0" Content="{x:Static resources:Strings.Menu_About_ReportIssue}" Icon="Chat48" NavigateUri="https://github.com/pizzaboxer/bloxstrap/issues" />
<ui:Anchor Margin="0,16,8,0" Content="{x:Static resources:Strings.Menu_About_HelpInformation}" Icon="BookQuestionMark24" NavigateUri="https://github.com/pizzaboxer/bloxstrap/wiki" />
<ui:Anchor Margin="0,16,0,0" Content="{x:Static resources:Strings.Menu_About_DiscordServer}" Icon="Chat48" NavigateUri="https://discord.gg/nKjV3mGq6R" />
</WrapPanel>
<StackPanel Visibility="{Binding BuildInformationVisibility, Mode=OneTime}">
<TextBlock Text="Build Information" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="hmmmm" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Column="0" Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Timestamp" />
<TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildTimestamp, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Machine" />
<TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.Machine, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Hash" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="2" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}">
<Hyperlink Foreground="{DynamicResource TextFillColorTertiaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="{Binding BuildCommitHashUrl, Mode=OneTime}">
<TextBlock Text="{Binding BuildMetadata.CommitHash, Mode=OneTime}" />
</Hyperlink>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Ref" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.CommitRef, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
</Grid>
</StackPanel>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:Expander Grid.Row="0" Grid.ColumnSpan="3" Margin="0,0,0,8" HeaderIcon="Translate24" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_Translations}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Bahasa Indonesia" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="e7leopard" />
<TextBlock Text="hfzrk" />
<TextBlock Text="soudblox" />
<TextBlock Text="Bokmål" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="darkevilmage" />
<TextBlock Text="endsouls" />
<TextBlock Text="letoek" />
<!--<TextBlock Text="Čeština" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Chusaak" />
<TextBlock Text="DanyCraftCZ" />
<TextBlock Text="Franklin_Surten" />
<TextBlock Text="Galaxy_Gangster6" />
<TextBlock Text="jasperholo" />
<TextBlock Text="letoek" />
<TextBlock Text="noobkid4545" />
<TextBlock Text="radim776" />-->
<!-- <TextBlock Text="Dansk" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Momslayer98" />
<TextBlock Text="SirBlue" /> -->
<TextBlock Text="Deutsch" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="agent_phoenix" />
<TextBlock Text="avoidr" />
<TextBlock Text="hcjohnsgd" />
<TextBlock Text="hxmbt" />
<TextBlock Text="kale123" />
<TextBlock Text="Marvin_Chu" />
<TextBlock Text="nzxt_xll" />
<TextBlock Text="Nlx095" />
<TextBlock Text="Ph1lwtf" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="TEAM_LILA" />
<TextBlock Text="xDevoidx" />
<TextBlock Text="Español" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="4Xisty" />
<TextBlock Text="Botkid" />
<TextBlock Text="colocky" />
<TextBlock Text="D0N-B0T" />
<TextBlock Text="Dasp" />
<TextBlock Text="devyyxn" />
<TextBlock Text="ItzzExcel" />
<TextBlock Text="Ilayhlinda" />
<TextBlock Text="Ilushiouss" />
<TextBlock Text="jayces." />
<TextBlock Text="kroesufos" />
<TextBlock Text="LaiyLiod" />
<TextBlock Text="lyalekin" />
<TextBlock Text="NezumiDS" />
<TextBlock Text="NimuruDP" />
<TextBlock Text="NescafeCL" />
<TextBlock Text="Sw7gger" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="Urzy" />
<TextBlock Text="Filipino" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alphjectiom" />
<TextBlock Text="FlaminDaPotato" />
<TextBlock Text="RobiTheRobloxxer" />
<TextBlock Text="shadow01148" />
<TextBlock Text="Français" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="At0zDx" />
<TextBlock Text="built4aiming" />
<TextBlock Text="hahaloserz360" />
<TextBlock Text="K0ga" />
<TextBlock Text="Marcssebaa" />
<TextBlock Text="MommySernox" />
<TextBlock Text="owentempest8" />
<TextBlock Text="Subsical" />
<TextBlock Text="thatsirwaffles" />
<TextBlock Text="tyundrai" />
<TextBlock Text="Waza80" />
<TextBlock Text="Hindi (Latin)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="kunaljainop" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="Sur_" />
<TextBlock Text="Tezos" />
<TextBlock Text="TheTakuo" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Hrvatski" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Dzigos" />
<TextBlock Text="Koyroii" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Italiano" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="crow_zxcr" />
<TextBlock Text="devyyxn" />
<TextBlock Text="domenicoiacono" />
<TextBlock Text="hulabulaseop" />
<TextBlock Text="lord_moth" />
<TextBlock Text="loridori" />
<TextBlock Text="Lupo01" />
<TextBlock Text="Mogunars" />
<TextBlock Text="pave08" />
<TextBlock Text="spectrumbruh" />
<TextBlock Text="Lietuvių" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="cr0000142" />
<TextBlock Text="Duexo" />
<TextBlock Text="jessethedev" />
<TextBlock Text="Vac31." />
<TextBlock Text="Magyar" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="DynoPlays" />
<TextBlock Text="Elotomka" />
<TextBlock Text="xM4rk1" />
<!--<TextBlock Text="Nederlands" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cosmix" />
<TextBlock Text="Miwwzy" />
<TextBlock Text="Quickvision1" />
<TextBlock Text="ydboss" />-->
<TextBlock Text="Polski" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Eztaru" />
<TextBlock Text="lunar" />
<TextBlock Text="markontm" />
<TextBlock Text="my5q" />
<TextBlock Text="nemzik2137" />
<TextBlock Text="plexar" />
<TextBlock Text="r.efil" />
<TextBlock Text="Português (Brasil)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="anormalevis" />
<TextBlock Text="ErisvaldoBalbino" />
<TextBlock Text="G3xneric" />
<TextBlock Text="hnter" />
<TextBlock Text="issei_" />
<TextBlock Text="iyto.lk" />
<TextBlock Text="jhermesn" />
<TextBlock Text="JorgeDaPelada" />
<TextBlock Text="LwgoDev" />
<TextBlock Text="nunk7" />
<TextBlock Text="peke7374" />
<TextBlock Text="SeeF" />
<TextBlock Text="Snowzin1" />
<TextBlock Text="storm930" />
<TextBlock Text="toofastforboo" />
<TextBlock Text="VMOICE" />
<TextBlock Text="Ye4" />
<TextBlock Text="ZaPeZaPe" />
<TextBlock Text="Română" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Externalkinetics" />
<TextBlock Text="MonochromeAlex" />
<TextBlock Text="PlayerValley" />
<TextBlock Text="Smuki" />
<TextBlock Text="theflopperguy" />
<TextBlock Text="Suomi" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="gelaxiz" />
<TextBlock Text="jes5" />
<TextBlock Text="retromaxwell" />
<TextBlock Text="SomePinglord" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Svenska" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Axellse" />
<TextBlock Text="CroppingFlea479" />
<TextBlock Text="FishySpelar" />
<TextBlock Text="PineappleSnackz" />
<TextBlock Text="simonixen" />
<TextBlock Text="thatgurkangurk" />
<TextBlock Text="Tiếng Việt" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alexking2068" />
<TextBlock Text="baterblx" />
<TextBlock Text="Elytronn" />
<TextBlock Text="fox810891" />
<TextBlock Text="ItsPoofy" />
<TextBlock Text="Limer1" />
<TextBlock Text="makayu203332" />
<TextBlock Text="MEx2/j7x6" />
<TextBlock Text="NguyenDat208" />
<TextBlock Text="quanmequankk" />
<TextBlock Text="SomeRandomGuy175" />
<TextBlock Text="SonThanhVN" />
<TextBlock Text="teaanguyenn" />
<TextBlock Text="Veiiorra" />
<TextBlock Text="Türkçe" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="canny19133" />
<TextBlock Text="cfors55" />
<TextBlock Text="drakfreddy" />
<TextBlock Text="enisify" />
<TextBlock Text="jayces." />
<TextBlock Text="nyatie" />
<TextBlock Text="PixelArmy" />
<TextBlock Text="plants8332" />
<TextBlock Text="r02" />
<TextBlock Text="siyamicik" />
<TextBlock Text="ydboss" />
<TextBlock Text="Українська" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="9zh" />
<TextBlock Text="andrey3569s" />
<TextBlock Text="DexterBloxxer" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="maksimvlad7" />
<TextBlock Text="Босански" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cortex_1" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Ren" />
<TextBlock Text="Български" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="GrafitNiki" />
<TextBlock Text="sidefrappe" />
<TextBlock Text="Русский" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="3tcy" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="andBroz" />
<TextBlock Text="alexneonwithglue" />
<TextBlock Text="AnonymousDudeLOL123" />
<TextBlock Text="aperna_of_the_ticks" />
<TextBlock Text="arsenijveselov77" />
<TextBlock Text="Art3mLapa" />
<TextBlock Text="cherkash" />
<TextBlock Text="cub-has-injected" />
<TextBlock Text="dallyuser" />
<TextBlock Text="Dr1mG" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="fxstyxx" />
<TextBlock Text="Gustodd4202" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="harababura" />
<TextBlock Text="ImperialRhyme" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="IStoleYourCheese" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="khat7" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="kostyan" />
<TextBlock Text="Maks" />
<TextBlock Text="niktoyou" />
<TextBlock Text="nurgament2" />
<TextBlock Text="poflexim" />
<TextBlock Text="Prob1rka" />
<TextBlock Text="Provo" />
<TextBlock Text="Quenevelly" />
<TextBlock Text="sally13249" />
<TextBlock Text="simmon8800" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Skylan031" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Spuffio" />
<TextBlock Text="StraiF" />
<TextBlock Text="StrayCatSimb" />
<TextBlock Text="Voxel" />
<TextBlock Text="XonaShera" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Ziio123" />
<TextBlock Text="zor9na90000" />
</StackPanel>
<StackPanel Grid.Column="3">
<TextBlock Text="עברית" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="1cur1" />
<TextBlock Text="ilan0098" />
<TextBlock Text="koerga" />
<TextBlock Text="Sezei" />
<TextBlock Text="العربية" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="busguesjshbahsj" />
<TextBlock Text="cq2i" />
<TextBlock Text="mmiky" />
<TextBlock Text="mostafagamingx1" />
<TextBlock Text="RoRed" />
<TextBlock Text="Sakupen" />
<TextBlock Text="streoic" />
<TextBlock Text="uvq18" />
<TextBlock Text="wyfast" />
<TextBlock Text="Zida" />
<TextBlock Text="বাংলা" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Arnian" />
<TextBlock Text="Hydrated_panda" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="red_hi" />
<TextBlock Text="ภาษาไทย" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="._bonus_." />
<TextBlock Text="marc15772" />
<TextBlock Text="arthurwagon" />
<TextBlock Text="Sem1z" />
<TextBlock Text="xAom" />
<TextBlock Text="한국어" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="ADVI50R" />
<TextBlock Text="asd123456fghqwerty" />
<TextBlock Text="bacon1295" />
<TextBlock Text="NightPlay" />
<TextBlock Text="中文 (简体)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="Aling00" />
<TextBlock Text="Clock" />
<TextBlock Text="ERSN_CERROR" />
<TextBlock Text="Kirxvil" />
<TextBlock Text="Typel" />
<TextBlock Text="yuhaodatt" />
<TextBlock Text="中文 (廣東話)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="henrychu1125" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="shhh_op" />
<TextBlock Text="中文 (繁體)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="DXuwu" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="日本語" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="MintJapan" />
<TextBlock Text="pimeja7" />
<TextBlock Text="yixhuaa" />
</StackPanel>
</Grid>
</controls:Expander>
<controls:Expander Grid.Row="1" Grid.Column="0" Margin="0,0,4,0" HeaderIcon="Code24" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_Code}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[Matt](https://github.com/bluepilledgreat)" />
<controls:MarkdownTextBlock MarkdownText="[1011025m](https://github.com/1011025m)" />
<controls:MarkdownTextBlock MarkdownText="[EasternBloxxer](https://github.com/EasternBloxxer)" />
<controls:MarkdownTextBlock MarkdownText="[sitiom](https://github.com/sitiom)" />
<controls:MarkdownTextBlock MarkdownText="[Extravi](https://github.com/Extravi)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[swatTurret](https://github.com/swatTurret)" />
<controls:MarkdownTextBlock MarkdownText="[fxeP1](https://github.com/fxeP1)" />
<controls:MarkdownTextBlock MarkdownText="[Redusofficial](https://github.com/Redusofficial)" />
<controls:MarkdownTextBlock MarkdownText="[srthMD](https://github.com/srthMD)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Row="1" Grid.Column="1" Margin="4,0,4,0" HeaderIcon="AppsAddIn28" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_FeatureIdeas}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[he3als](https://github.com/he3als)" />
<controls:MarkdownTextBlock MarkdownText="[NikSavchenk0](https://github.com/NikSavchenk0)" />
<controls:MarkdownTextBlock MarkdownText="[carter0nline](https://github.com/carter0nline)" />
<controls:MarkdownTextBlock MarkdownText="[lolmanurfunny](https://github.com/lolmanurfunny)" />
<controls:MarkdownTextBlock MarkdownText="[MehKako](https://github.com/MehKako)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[knivesofeylis](https://github.com/knivesofeylis)" />
<controls:MarkdownTextBlock MarkdownText="[sha4owz](https://github.com/sha4owz)" />
<controls:MarkdownTextBlock MarkdownText="[DaMlgNoodle](https://github.com/DaMlgNoodle)" />
<controls:MarkdownTextBlock MarkdownText="[nakoyasha](https://github.com/nakoyasha)" />
<controls:MarkdownTextBlock MarkdownText="[exurd](https://github.com/exurd)" />
<controls:MarkdownTextBlock MarkdownText="[0xFE0F](https://github.com/0xFE0F)" />
<controls:MarkdownTextBlock MarkdownText="[Tezos](https://github.com/GoingCrazyDude)" />
<controls:MarkdownTextBlock MarkdownText="[CfwSky](https://www.roblox.com/users/129425241/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ruubloo](https://www.roblox.com/users/158082266/profile)" />
<controls:MarkdownTextBlock MarkdownText="[toyoda165](https://www.roblox.com/users/923416649/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ShadowCodeX](https://github.com/ShadowCodeX-debug)" />
<controls:MarkdownTextBlock MarkdownText="[cub-has-injected](https://github.com/cub-has-injected)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Row="1" Grid.Column="2" Margin="4,0,0,0" HeaderIcon="Heart16" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_SpecialThanks}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[MaximumADHD](https://github.com/MaximumADHD)" />
<controls:MarkdownTextBlock MarkdownText="[Multako](https://www.roblox.com/users/2485612194/profile)" />
<controls:MarkdownTextBlock MarkdownText="[axstin](https://github.com/axstin)" />
<controls:MarkdownTextBlock MarkdownText="[taskmanager](https://github.com/Mantaraix)" />
<controls:MarkdownTextBlock MarkdownText="[apprehensions](https://github.com/apprehensions)" />
</StackPanel>
</controls:Expander>
</Grid>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Licenses}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Grid.Row="0" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="Bloxstrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/lepoco/wpfui/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="WPF-UI" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/securifybv/ShellLink/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="14" Text="ShellLink" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="DiscordRPC" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="13" Text="SharpZipLib" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<StackPanel>
<TextBlock FontSize="14" Text="Markdig" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -137,7 +137,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
} }
else else
{ {
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information); Frontend.ShowMessageBox(Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information);
bool refresh = false; bool refresh = false;
@ -175,7 +175,14 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
json = '{' + json; json = '{' + json;
if (!json.EndsWith('}')) if (!json.EndsWith('}'))
{
int lastIndex = json.LastIndexOf('}');
if (lastIndex == -1)
json += '}'; json += '}';
else
json = json.Substring(0, lastIndex+1);
}
try try
{ {
@ -193,7 +200,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
catch (Exception ex) catch (Exception ex)
{ {
Frontend.ShowMessageBox( Frontend.ShowMessageBox(
String.Format(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidJSON, ex.Message), String.Format(Strings.Menu_FastFlagEditor_InvalidJSON, ex.Message),
MessageBoxImage.Error MessageBoxImage.Error
); );
@ -205,7 +212,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
if (list.Count > 16) if (list.Count > 16)
{ {
var result = Frontend.ShowMessageBox( var result = Frontend.ShowMessageBox(
Bloxstrap.Resources.Strings.Menu_FastFlagEditor_LargeConfig, Strings.Menu_FastFlagEditor_LargeConfig,
MessageBoxImage.Warning, MessageBoxImage.Warning,
MessageBoxButton.YesNo MessageBoxButton.YesNo
); );
@ -222,7 +229,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
int count = conflictingFlags.Count(); int count = conflictingFlags.Count();
string message = String.Format( string message = String.Format(
Bloxstrap.Resources.Strings.Menu_FastFlagEditor_ConflictingImport, Strings.Menu_FastFlagEditor_ConflictingImport,
count, count,
String.Join(", ", conflictingFlags.Take(25)) String.Join(", ", conflictingFlags.Take(25))
); );
@ -263,16 +270,16 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
string errorMessage = ""; string errorMessage = "";
if (!_validPrefixes.Any(name.StartsWith)) if (!_validPrefixes.Any(name.StartsWith))
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidPrefix; errorMessage = Strings.Menu_FastFlagEditor_InvalidPrefix;
else if (!name.All(x => char.IsLetterOrDigit(x) || x == '_')) else if (!name.All(x => char.IsLetterOrDigit(x) || x == '_'))
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidCharacter; errorMessage = Strings.Menu_FastFlagEditor_InvalidCharacter;
if (name.EndsWith("_PlaceFilter") || name.EndsWith("_DataCenterFilter")) if (name.EndsWith("_PlaceFilter") || name.EndsWith("_DataCenterFilter"))
errorMessage = !ValidateFilter(name, value) ? Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidPlaceFilter : ""; errorMessage = !ValidateFilter(name, value) ? Strings.Menu_FastFlagEditor_InvalidPlaceFilter : "";
else if ((name.StartsWith("FInt") || name.StartsWith("DFInt")) && !Int32.TryParse(value, out _)) else if ((name.StartsWith("FInt") || name.StartsWith("DFInt")) && !Int32.TryParse(value, out _))
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidNumberValue; errorMessage = Strings.Menu_FastFlagEditor_InvalidNumberValue;
else if ((name.StartsWith("FFlag") || name.StartsWith("DFFlag")) && lowerValue != "true" && lowerValue != "false") else if ((name.StartsWith("FFlag") || name.StartsWith("DFFlag")) && lowerValue != "true" && lowerValue != "false")
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidBoolValue; errorMessage = Strings.Menu_FastFlagEditor_InvalidBoolValue;
if (!String.IsNullOrEmpty(errorMessage)) if (!String.IsNullOrEmpty(errorMessage))
{ {
@ -300,12 +307,10 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{ {
int index = e.Row.GetIndex(); if (e.Row.DataContext is not FastFlag entry)
FastFlag entry = _fastFlagList[index]; return;
var textbox = e.EditingElement as TextBox; if (e.EditingElement is not TextBox textbox)
if (textbox is null)
return; return;
switch (e.Column.Header) switch (e.Column.Header)
@ -319,7 +324,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
if (App.FastFlags.GetValue(newName) is not null) if (App.FastFlags.GetValue(newName) is not null)
{ {
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information); Frontend.ShowMessageBox(Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information);
e.Cancel = true; e.Cancel = true;
textbox.Text = oldName; textbox.Text = oldName;
return; return;
@ -387,7 +392,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
{ {
string json = JsonSerializer.Serialize(App.FastFlags.Prop, new JsonSerializerOptions { WriteIndented = true }); string json = JsonSerializer.Serialize(App.FastFlags.Prop, new JsonSerializerOptions { WriteIndented = true });
Clipboard.SetDataObject(json); Clipboard.SetDataObject(json);
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_JsonCopiedToClipboard, MessageBoxImage.Information); Frontend.ShowMessageBox(Strings.Menu_FastFlagEditor_JsonCopiedToClipboard, MessageBoxImage.Information);
} }
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)

View File

@ -86,7 +86,7 @@
Header="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Title}" Header="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Title}"
Description="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Description}" Description="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Description}"
HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#framerate-limit"> HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#framerate-limit">
<ui:TextBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" Text="{Binding FramerateLimit, Mode=TwoWay}" PreviewTextInput="ValidateInt32" /> <ui:TextBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" Text="{Binding FramerateLimit, Mode=TwoWay}" PreviewTextInput="ValidateUInt32" />
</controls:OptionControl> </controls:OptionControl>
<controls:OptionControl <controls:OptionControl
@ -165,12 +165,6 @@
</ComboBox> </ComboBox>
</controls:OptionControl> </controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_FastFlags_Presets_AltGraphicsSelector_Title}"
Description="{x:Static resources:Strings.Menu_FastFlags_Presets_AltGraphicsSelector_Description}">
<ui:ToggleSwitch IsChecked="{Binding AlternateGraphicsSelectorEnabled, Mode=TwoWay}" />
</controls:OptionControl>
<ui:CardAction Margin="0,24,0,0" Icon="EditSettings24" Command="{Binding OpenFastFlagEditorCommand}"> <ui:CardAction Margin="0,24,0,0" Icon="EditSettings24" Command="{Binding OpenFastFlagEditorCommand}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_FastFlagEditor_Title}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_FastFlagEditor_Title}" />

View File

@ -31,6 +31,8 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
DataContext = new FastFlagsViewModel(this); DataContext = new FastFlagsViewModel(this);
} }
private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = !Int32.TryParse(e.Text, out int _); private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = e.Text != "-" && !Int32.TryParse(e.Text, out int _);
private void ValidateUInt32(object sender, TextCompositionEventArgs e) => e.Handled = !UInt32.TryParse(e.Text, out uint _);
} }
} }

View File

@ -26,8 +26,9 @@
</controls:OptionControl> </controls:OptionControl>
<controls:OptionControl <controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_ShowServerDetails_Title}" Header="{x:Static resources:Strings.Menu_Integrations_QueryServerLocation_Title}"
Description="{x:Static resources:Strings.Menu_Integrations_ShowServerDetails_Description}" Description="{Binding Source={x:Static resources:Strings.Menu_Integrations_QueryServerLocation_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://ipinfo.io'}"
HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/What-is-activity-tracking%3F#server-location-querying"
IsEnabled="{Binding InnerContent.IsChecked, ElementName=ActivityTrackingOption, Mode=OneWay}"> IsEnabled="{Binding InnerContent.IsChecked, ElementName=ActivityTrackingOption, Mode=OneWay}">
<ui:ToggleSwitch IsChecked="{Binding ShowServerDetailsEnabled, Mode=TwoWay}" /> <ui:ToggleSwitch IsChecked="{Binding ShowServerDetailsEnabled, Mode=TwoWay}" />
</controls:OptionControl> </controls:OptionControl>
@ -40,7 +41,7 @@
</controls:OptionControl> </controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Common_DiscordRichPresence}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" /> <TextBlock Text="{x:Static resources:Strings.Common_DiscordRichPresence}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_Integrations_RequiresActivityTracking}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" /> <controls:MarkdownTextBlock MarkdownText="{Binding Source={x:Static resources:Strings.Menu_Integrations_RequiresActivityTracking}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://github.com/pizzaboxer/bloxstrap/wiki/What-is-activity-tracking%3F#discord-rich-presence'}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<controls:OptionControl <controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_ShowGameActivity_Title}" Header="{x:Static resources:Strings.Menu_Integrations_ShowGameActivity_Title}"
@ -88,18 +89,18 @@
</Style> </Style>
</StackPanel.Style> </StackPanel.Style>
<TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" /> <TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomIntegration.Name}" /> <ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomIntegration.Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_AppLocation}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" /> <TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_AppLocation}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,4,0,0"> <Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ui:TextBox Grid.Column="0" Margin="0,0,0,0" PlaceholderText="{x:Static resources:Strings.Menu_Integrations_Custom_AppLocation_Placeholder}" Text="{Binding SelectedCustomIntegration.Location}" /> <ui:TextBox Grid.Column="0" Margin="0,0,0,0" PlaceholderText="C:\Windows\System32\cmd.exe" Text="{Binding SelectedCustomIntegration.Location}" />
<ui:Button Grid.Column="1" Margin="8,0,0,0" Height="34" Icon="Folder24" Content="{x:Static resources:Strings.Common_Browse}" Command="{Binding BrowseIntegrationLocationCommand}" /> <ui:Button Grid.Column="1" Margin="8,0,0,0" Height="34" Icon="Folder24" Content="{x:Static resources:Strings.Common_Browse}" Command="{Binding BrowseIntegrationLocationCommand}" />
</Grid> </Grid>
<TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" /> <TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" PlaceholderText="{x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs_Placeholder}" Text="{Binding SelectedCustomIntegration.LaunchArgs}" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" /> <ui:TextBox Margin="0,4,0,0" PlaceholderText="{Binding Source='/k echo {0}', Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs_Placeholder}}" Text="{Binding SelectedCustomIntegration.LaunchArgs}" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" />
<CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_AutoClose}" IsChecked="{Binding SelectedCustomIntegration.AutoClose}" /> <CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_AutoClose}" IsChecked="{Binding SelectedCustomIntegration.AutoClose}" />
</StackPanel> </StackPanel>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="{x:Static resources:Strings.Menu_Integrations_Custom_NoneSelected}" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center"> <TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="{x:Static resources:Strings.Menu_Integrations_Custom_NoneSelected}" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">

View File

@ -25,18 +25,27 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ui:CardAction Grid.Row="0" Grid.Column="0" x:Name="OpenModFolderCardAction" Margin="0,0,4,0" Icon="Folder24" Command="{Binding OpenModsFolderCommand}"> <ui:CardAction Grid.Row="0" Grid.Column="0" x:Name="OpenModFolderCardAction" Margin="0,0,4,0" Icon="Folder24" Command="{Binding OpenModsFolderCommand}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Title}" TextWrapping="Wrap" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Title}" TextWrapping="Wrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" /> <TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</ui:CardAction> </ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="1" Margin="4,0,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/Adding-custom-mods"> <ui:CardAction Grid.Row="0" Grid.Column="1" Margin="4,0,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/Adding-custom-mods">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Common_Help}" /> <TextBlock FontSize="14" Text="{x:Static resources:Strings.Common_Help}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Mods_Help_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" /> <TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Mods_Help_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel> </StackPanel>
</ui:CardAction> </ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Icon="WindowWrench24" Command="{Binding OpenCompatSettingsCommand}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Mods_Misc_CompatibilitySettings_Title}" TextWrapping="Wrap" />
<controls:MarkdownTextBlock Margin="0,2,0,0" FontSize="12" MarkdownText="{Binding Source={x:Static resources:Strings.Menu_Mods_Misc_CompatibilitySettings_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://devblogs.microsoft.com/directx/demystifying-full-screen-optimizations/'}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel>
</ui:CardAction>
</Grid> </Grid>
<TextBlock Text="{x:Static resources:Strings.Common_Presets}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" /> <TextBlock Text="{x:Static resources:Strings.Common_Presets}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
@ -93,13 +102,5 @@
<ui:Button Icon="Delete16" Content="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Remove}" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" /> <ui:Button Icon="Delete16" Content="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Remove}" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" />
</StackPanel> </StackPanel>
</controls:OptionControl> </controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Misc_DisableFullscreenOptimisations_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Misc_DisableFullscreenOptimisations_Description}"
HelpLink="https://devblogs.microsoft.com/directx/demystifying-full-screen-optimizations/"
x:Name="FullscreenOptimizationsToggle">
<ui:ToggleSwitch IsChecked="{Binding DisableFullscreenOptimizations, Mode=TwoWay}" />
</controls:OptionControl>
</StackPanel> </StackPanel>
</ui:UiPage> </ui:UiPage>

View File

@ -1,6 +1,4 @@
using System.Windows; using Bloxstrap.UI.ViewModels.Settings;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Settings.Pages namespace Bloxstrap.UI.Elements.Settings.Pages
{ {
@ -13,10 +11,6 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
{ {
DataContext = new ModsViewModel(); DataContext = new ModsViewModel();
InitializeComponent(); InitializeComponent();
// fullscreen optimizations were only added in windows 10 build 17093
if (Environment.OSVersion.Version.Build < 17093)
this.FullscreenOptimizationsToggle.Visibility = Visibility.Collapsed;
} }
} }
} }

View File

@ -17,35 +17,42 @@ namespace Bloxstrap.UI
if (App.LaunchSettings.QuietFlag.Active) if (App.LaunchSettings.QuietFlag.Active)
return defaultResult; return defaultResult;
if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
return ShowFluentMessageBox(message, icon, buttons); return ShowFluentMessageBox(message, icon, buttons);
switch (App.Settings.Prop.BootstrapperStyle)
{
case BootstrapperStyle.FluentDialog:
case BootstrapperStyle.ClassicFluentDialog:
case BootstrapperStyle.FluentAeroDialog:
case BootstrapperStyle.ByfronDialog:
return ShowFluentMessageBox(message, icon, buttons);
default:
return MessageBox.Show(message, App.ProjectName, buttons, icon);
} }
public static void ShowPlayerErrorDialog(bool crash = false)
{
if (App.LaunchSettings.QuietFlag.Active)
return;
string topLine = Strings.Dialog_PlayerError_FailedLaunch;
if (crash)
topLine = Strings.Dialog_PlayerError_Crash;
ShowMessageBox($"{topLine}\n\n{Strings.Dialog_PlayerError_HelpInformation}", MessageBoxImage.Error);
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch");
} }
public static void ShowExceptionDialog(Exception exception) public static void ShowExceptionDialog(Exception exception)
{ {
if (App.LaunchSettings.QuietFlag.Active)
return;
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
new ExceptionDialog(exception).ShowDialog(); new ExceptionDialog(exception).ShowDialog();
}); });
} }
public static void ShowConnectivityDialog(string title, string description, Exception exception) public static void ShowConnectivityDialog(string title, string description, MessageBoxImage image, Exception exception)
{ {
if (App.LaunchSettings.QuietFlag.Active)
return;
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
new ConnectivityDialog(title, description, exception).ShowDialog(); new ConnectivityDialog(title, description, image, exception).ShowDialog();
}); });
} }

View File

@ -1,4 +1,5 @@
using Bloxstrap.Integrations; using Bloxstrap.Integrations;
using Bloxstrap.UI.Elements.About;
using Bloxstrap.UI.Elements.ContextMenu; using Bloxstrap.UI.Elements.ContextMenu;
namespace Bloxstrap.UI namespace Bloxstrap.UI
@ -10,19 +11,22 @@ namespace Bloxstrap.UI
private bool _disposing = false; private bool _disposing = false;
private readonly System.Windows.Forms.NotifyIcon _notifyIcon; private readonly System.Windows.Forms.NotifyIcon _notifyIcon;
private MenuContainer? _menuContainer;
private ActivityWatcher? _activityWatcher; private readonly MenuContainer _menuContainer;
private DiscordRichPresence? _richPresenceHandler;
private int? _processId; private readonly Watcher _watcher;
private ActivityWatcher? _activityWatcher => _watcher.ActivityWatcher;
EventHandler? _alertClickHandler; EventHandler? _alertClickHandler;
public NotifyIconWrapper() public NotifyIconWrapper(Watcher watcher)
{ {
App.Logger.WriteLine("NotifyIconWrapper::NotifyIconWrapper", "Initializing notification area icon"); App.Logger.WriteLine("NotifyIconWrapper::NotifyIconWrapper", "Initializing notification area icon");
_notifyIcon = new() _watcher = watcher;
_notifyIcon = new(new System.ComponentModel.Container())
{ {
Icon = Properties.Resources.IconBloxstrap, Icon = Properties.Resources.IconBloxstrap,
Text = App.ProjectName, Text = App.ProjectName,
@ -30,52 +34,18 @@ namespace Bloxstrap.UI
}; };
_notifyIcon.MouseClick += MouseClickEventHandler; _notifyIcon.MouseClick += MouseClickEventHandler;
if (_activityWatcher is not null && App.Settings.Prop.ShowServerDetails)
_activityWatcher.OnGameJoin += OnGameJoin;
_menuContainer = new(_watcher);
_menuContainer.Show();
} }
#region Handler registers
public void SetRichPresenceHandler(DiscordRichPresence richPresenceHandler)
{
if (_richPresenceHandler is not null)
return;
_richPresenceHandler = richPresenceHandler;
}
public void SetActivityWatcher(ActivityWatcher activityWatcher)
{
if (_activityWatcher is not null)
return;
_activityWatcher = activityWatcher;
if (App.Settings.Prop.ShowServerDetails)
_activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin);
}
public void SetProcessId(int processId)
{
if (_processId is not null)
return;
_processId = processId;
}
#endregion
#region Context menu #region Context menu
public void InitializeContextMenu()
{
if (_menuContainer is not null || _disposing)
return;
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
_menuContainer = new(_activityWatcher, _richPresenceHandler, _processId);
_menuContainer.ShowDialog();
}
public void MouseClickEventHandler(object? sender, System.Windows.Forms.MouseEventArgs e) public void MouseClickEventHandler(object? sender, System.Windows.Forms.MouseEventArgs e)
{ {
if (e.Button != System.Windows.Forms.MouseButtons.Right || _menuContainer is null) if (e.Button != System.Windows.Forms.MouseButtons.Right)
return; return;
_menuContainer.Activate(); _menuContainer.Activate();
@ -84,10 +54,17 @@ namespace Bloxstrap.UI
#endregion #endregion
#region Activity handlers #region Activity handlers
public async void OnGameJoin() public async void OnGameJoin(object? sender, EventArgs e)
{ {
string serverLocation = await _activityWatcher!.GetServerLocation(); if (_activityWatcher is null)
string title = _activityWatcher.ActivityServerType switch return;
string? serverLocation = await _activityWatcher.Data.QueryServerLocation();
if (string.IsNullOrEmpty(serverLocation))
return;
string title = _activityWatcher.Data.ServerType switch
{ {
ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public, ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,
ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private, ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private,
@ -99,11 +76,12 @@ namespace Bloxstrap.UI
title, title,
String.Format(Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation), String.Format(Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation),
10, 10,
(_, _) => _menuContainer?.ShowServerInformationWindow() (_, _) => _menuContainer.ShowServerInformationWindow()
); );
} }
#endregion #endregion
// we may need to create our own handler for this, because this sorta sucks
public void ShowAlert(string caption, string message, int duration, EventHandler? clickHandler) public void ShowAlert(string caption, string message, int duration, EventHandler? clickHandler)
{ {
string id = Guid.NewGuid().ToString()[..8]; string id = Guid.NewGuid().ToString()[..8];
@ -151,9 +129,8 @@ namespace Bloxstrap.UI
App.Logger.WriteLine("NotifyIconWrapper::Dispose", "Disposing NotifyIcon"); App.Logger.WriteLine("NotifyIconWrapper::Dispose", "Disposing NotifyIcon");
_menuContainer?.Dispatcher.Invoke(_menuContainer.Close); _menuContainer.Dispatcher.Invoke(_menuContainer.Close);
_notifyIcon?.Dispose(); _notifyIcon.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@ -0,0 +1,63 @@
using System.Windows;
namespace Bloxstrap.UI.ViewModels.About
{
public class AboutViewModel : NotifyPropertyChangedViewModel
{
private SupporterData? _supporterData;
public string Version => string.Format(Strings.Menu_About_Version, App.Version);
public BuildMetadataAttribute BuildMetadata => App.BuildMetadata;
public string BuildTimestamp => BuildMetadata.Timestamp.ToFriendlyString();
public string BuildCommitHashUrl => $"https://github.com/{App.ProjectRepository}/commit/{BuildMetadata.CommitHash}";
public Visibility BuildInformationVisibility => App.IsProductionBuild ? Visibility.Collapsed : Visibility.Visible;
public Visibility BuildCommitVisibility => App.IsActionBuild ? Visibility.Visible : Visibility.Collapsed;
public List<Supporter> Supporters => _supporterData?.Supporters ?? Enumerable.Empty<Supporter>().ToList();
public int SupporterColumns => _supporterData?.Columns ?? 0;
public GenericTriState SupportersLoadedState { get; set; } = GenericTriState.Unknown;
public string SupportersLoadError { get; set; } = "";
public AboutViewModel()
{
// this will cause momentary freezes only when ran under the debugger
LoadSupporterData();
}
public async void LoadSupporterData()
{
const string LOG_IDENT = "AboutViewModel::LoadSupporterData";
try
{
_supporterData = await Http.GetJson<SupporterData>("https://raw.githubusercontent.com/bloxstraplabs/config/main/supporters.json");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Could not load supporter data");
App.Logger.WriteException(LOG_IDENT, ex);
SupportersLoadedState = GenericTriState.Failed;
SupportersLoadError = ex.Message;
OnPropertyChanged(nameof(SupportersLoadError));
}
if (_supporterData is not null)
{
SupportersLoadedState = GenericTriState.Successful;
OnPropertyChanged(nameof(Supporters));
OnPropertyChanged(nameof(SupporterColumns));
}
OnPropertyChanged(nameof(SupportersLoadedState));
}
}
}

View File

@ -39,7 +39,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
private void CancelInstall() private void CancelInstall()
{ {
_dialog.Bootstrapper?.CancelInstall(); _dialog.Bootstrapper?.Cancel();
_dialog.CloseBootstrapper(); _dialog.CloseBootstrapper();
} }
} }

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