From 9ef6579a4163e2fb5b3c10ad16f588fd2a3c8947 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Fri, 14 Mar 2025 14:23:53 +0000
Subject: [PATCH 01/22] update ElementAttributeMissingChild string
---
Bloxstrap/Resources/Strings.Designer.cs | 2 +-
Bloxstrap/Resources/Strings.resx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs
index 69565c6..5395778 100644
--- a/Bloxstrap/Resources/Strings.Designer.cs
+++ b/Bloxstrap/Resources/Strings.Designer.cs
@@ -1082,7 +1082,7 @@ namespace Bloxstrap.Resources {
}
///
- /// Looks up a localized string similar to {0}.{1} is missing it's child.
+ /// Looks up a localized string similar to {0}.{1} is missing its child.
///
public static string CustomTheme_Errors_ElementAttributeMissingChild {
get {
diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx
index 20624a5..7c42f6a 100644
--- a/Bloxstrap/Resources/Strings.resx
+++ b/Bloxstrap/Resources/Strings.resx
@@ -1369,7 +1369,7 @@ Please close any applications that may be using Roblox's files, and relaunch.{0} is the element name (e.g. Button)
- {0}.{1} is missing it's child
+ {0}.{1} is missing its child
{0}.{1} is the element & attribute name (e.g. Button.Text)
From 338ebba191a850935015cbb80ac42486c173fd59 Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 00:17:23 +0000
Subject: [PATCH 02/22] Localise more custom dialog related strings (#4881)
* translate template comments
* localise default custom theme name
---
Bloxstrap/Extensions/CustomThemeTemplateEx.cs | 28 ++++++++++++++-
.../CustomBootstrapperTemplate_Blank.xml | 4 +--
.../CustomBootstrapperTemplate_Simple.xml | 2 +-
Bloxstrap/Resources/Strings.Designer.cs | 36 +++++++++++++++++++
Bloxstrap/Resources/Strings.resx | 13 +++++++
.../Dialogs/AddCustomThemeDialog.xaml.cs | 7 ++--
6 files changed, 83 insertions(+), 7 deletions(-)
diff --git a/Bloxstrap/Extensions/CustomThemeTemplateEx.cs b/Bloxstrap/Extensions/CustomThemeTemplateEx.cs
index 4088f02..ed9dcea 100644
--- a/Bloxstrap/Extensions/CustomThemeTemplateEx.cs
+++ b/Bloxstrap/Extensions/CustomThemeTemplateEx.cs
@@ -1,10 +1,36 @@
-namespace Bloxstrap.Extensions
+using System.Text;
+
+namespace Bloxstrap.Extensions
{
static class CustomThemeTemplateEx
{
+ const string EXAMPLES_URL = "https://github.com/bloxstraplabs/custom-bootstrapper-examples";
+
public static string GetFileName(this CustomThemeTemplate template)
{
return $"CustomBootstrapperTemplate_{template}.xml";
}
+
+ public static string GetFileContents(this CustomThemeTemplate template)
+ {
+ string contents = Encoding.UTF8.GetString(Resource.Get(template.GetFileName()).Result);
+
+ switch (template)
+ {
+ case CustomThemeTemplate.Blank:
+ {
+ string moreText = string.Format(Strings.CustomTheme_Templates_Blank_MoreExamples, EXAMPLES_URL);
+ return string.Format(contents, Strings.CustomTheme_Templates_Blank_UIElements, moreText);
+ }
+ case CustomThemeTemplate.Simple:
+ {
+ string moreText = string.Format(Strings.CustomTheme_Templates_Simple_MoreExamples, EXAMPLES_URL);
+ return string.Format(contents, moreText);
+ }
+ default:
+ Debug.Assert(false);
+ return contents;
+ }
+ }
}
}
diff --git a/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml b/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml
index 99efce9..3945086 100644
--- a/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml
+++ b/Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml
@@ -1,4 +1,4 @@
-
-
+
+
\ No newline at end of file
diff --git a/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml b/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml
index 7664806..abe586d 100644
--- a/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml
+++ b/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs
index 5395778..848b724 100644
--- a/Bloxstrap/Resources/Strings.Designer.cs
+++ b/Bloxstrap/Resources/Strings.Designer.cs
@@ -973,6 +973,15 @@ namespace Bloxstrap.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Custom Theme {0}.
+ ///
+ public static string CustomTheme_DefaultName {
+ get {
+ return ResourceManager.GetString("CustomTheme.DefaultName", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Save changes to {0}?.
///
@@ -1271,6 +1280,33 @@ namespace Bloxstrap.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Examples of custom bootstrappers can be found at {0}.
+ ///
+ public static string CustomTheme_Templates_Blank_MoreExamples {
+ get {
+ return ResourceManager.GetString("CustomTheme.Templates.Blank.MoreExamples", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Put UI elements here.
+ ///
+ public static string CustomTheme_Templates_Blank_UIElements {
+ get {
+ return ResourceManager.GetString("CustomTheme.Templates.Blank.UIElements", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Find more custom bootstrapper examples at {0}.
+ ///
+ public static string CustomTheme_Templates_Simple_MoreExamples {
+ get {
+ return ResourceManager.GetString("CustomTheme.Templates.Simple.MoreExamples", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Add Fast Flag.
///
diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx
index 7c42f6a..f0355ae 100644
--- a/Bloxstrap/Resources/Strings.resx
+++ b/Bloxstrap/Resources/Strings.resx
@@ -1474,4 +1474,17 @@ Defaulting to {1}.
Update Roblox in the background instead of waiting. Not recommended for slow networks. At least 3GB of free storage space is required for this feature to work.
+
+ Put UI elements here
+
+
+ Examples of custom bootstrappers can be found at {0}
+
+
+ Find more custom bootstrapper examples at {0}
+
+
+ Custom Theme {0}
+ {0} is a string (e.g. '1', '1-1234')
+
\ No newline at end of file
diff --git a/Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
index 94ce4a8..9d88c8c 100644
--- a/Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
+++ b/Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
@@ -39,11 +39,12 @@ namespace Bloxstrap.UI.Elements.Dialogs
{
int count = Directory.GetDirectories(Paths.CustomThemes).Count();
- string name = $"Custom Theme {count + 1}";
+ int i = count + 1;
+ string name = string.Format(Strings.CustomTheme_DefaultName, i);
// TODO: this sucks
if (File.Exists(GetThemePath(name)))
- name += " " + Random.Shared.Next(1, 100000).ToString(); // easy
+ name = string.Format(Strings.CustomTheme_DefaultName, $"{i}-{Random.Shared.Next(1, 100000)}"); // easy
return name;
}
@@ -76,7 +77,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
string themeFilePath = Path.Combine(dir, "Theme.xml");
- string templateContent = Encoding.UTF8.GetString(Resource.Get(template.GetFileName()).Result);
+ string templateContent = template.GetFileContents();
File.WriteAllText(themeFilePath, templateContent);
}
From f0eb2eb745facb0d153ccf57f5fcb2e47f9baa36 Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 10:52:10 +0000
Subject: [PATCH 03/22] use `PathValidator` for `RenameCustomTheme` (#4886)
---
.../Settings/AppearanceViewModel.cs | 42 +++++++++++++++++--
1 file changed, 39 insertions(+), 3 deletions(-)
diff --git a/Bloxstrap/UI/ViewModels/Settings/AppearanceViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/AppearanceViewModel.cs
index d09f23c..0870bfb 100644
--- a/Bloxstrap/UI/ViewModels/Settings/AppearanceViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Settings/AppearanceViewModel.cs
@@ -194,11 +194,47 @@ namespace Bloxstrap.UI.ViewModels.Settings
private void RenameCustomTheme()
{
- if (SelectedCustomTheme is null)
+ const string LOG_IDENT = "AppearanceViewModel::RenameCustomTheme";
+
+ if (SelectedCustomTheme is null || SelectedCustomTheme == SelectedCustomThemeName)
return;
- if (SelectedCustomTheme == SelectedCustomThemeName)
+ if (string.IsNullOrEmpty(SelectedCustomThemeName))
+ {
+ Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameEmpty, MessageBoxImage.Error);
return;
+ }
+
+ var validationResult = PathValidator.IsFileNameValid(SelectedCustomThemeName);
+
+ if (validationResult != PathValidator.ValidationResult.Ok)
+ {
+ switch (validationResult)
+ {
+ case PathValidator.ValidationResult.IllegalCharacter:
+ Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameIllegalCharacters, MessageBoxImage.Error);
+ break;
+ case PathValidator.ValidationResult.ReservedFileName:
+ Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameReserved, MessageBoxImage.Error);
+ break;
+ default:
+ App.Logger.WriteLine(LOG_IDENT, $"Got unhandled PathValidator::ValidationResult {validationResult}");
+ Debug.Assert(false);
+
+ Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_Unknown, MessageBoxImage.Error);
+ break;
+ }
+
+ return;
+ }
+
+ // better to check for the file instead of the directory so broken themes can be overwritten
+ string path = Path.Combine(Paths.CustomThemes, SelectedCustomThemeName, "Theme.xml");
+ if (File.Exists(path))
+ {
+ Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameTaken, MessageBoxImage.Error);
+ return;
+ }
try
{
@@ -206,7 +242,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
}
catch (Exception ex)
{
- App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
+ App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_RenameFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
return;
}
From d0f1b9de226a59b1b8791a41f786000ae8450b69 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 13:04:10 +0000
Subject: [PATCH 04/22] toggle `OpenReleaseNotes`
---
Bloxstrap/Installer.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs
index d57e985..8d1a6e6 100644
--- a/Bloxstrap/Installer.cs
+++ b/Bloxstrap/Installer.cs
@@ -9,7 +9,7 @@ namespace Bloxstrap
/// Should this version automatically open the release notes page?
/// Recommended for major updates only.
///
- private const bool OpenReleaseNotes = false;
+ private const bool OpenReleaseNotes = true;
private static string DesktopShortcut => Path.Combine(Paths.Desktop, $"{App.ProjectName}.lnk");
From 49fd8eb2d2f860f3a80838b7fd228bdb00ebd5b5 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 14:35:09 +0000
Subject: [PATCH 05/22] improve `Watcher` ctor macro exclusions
---
Bloxstrap/Watcher.cs | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/Bloxstrap/Watcher.cs b/Bloxstrap/Watcher.cs
index eef397e..d56c95f 100644
--- a/Bloxstrap/Watcher.cs
+++ b/Bloxstrap/Watcher.cs
@@ -28,21 +28,21 @@ namespace Bloxstrap
string? watcherDataArg = App.LaunchSettings.WatcherFlag.Data;
-#if DEBUG
if (String.IsNullOrEmpty(watcherDataArg))
{
+#if DEBUG
string path = new RobloxPlayerData().ExecutablePath;
using var gameClientProcess = Process.Start(path);
_watcherData = new() { ProcessId = gameClientProcess.Id };
- }
#else
- if (String.IsNullOrEmpty(watcherDataArg))
throw new Exception("Watcher data not specified");
#endif
-
- if (!String.IsNullOrEmpty(watcherDataArg))
+ }
+ else
+ {
_watcherData = JsonSerializer.Deserialize(Encoding.UTF8.GetString(Convert.FromBase64String(watcherDataArg)));
+ }
if (_watcherData is null)
throw new Exception("Watcher data is invalid");
From d244f42b49ed3f12764e32f720e6118f5c869dd6 Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 17:42:21 +0000
Subject: [PATCH 06/22] Reintroduce multi-instance launching (#4888)
---
Bloxstrap/Bootstrapper.cs | 41 +++++++++--
Bloxstrap/LaunchHandler.cs | 29 +++++++-
Bloxstrap/LaunchSettings.cs | 30 ++++----
Bloxstrap/Models/Persistable/Settings.cs | 1 +
Bloxstrap/MultiInstanceWatcher.cs | 68 +++++++++++++++++++
Bloxstrap/Resources/Strings.Designer.cs | 18 +++++
Bloxstrap/Resources/Strings.resx | 6 ++
.../Settings/Pages/IntegrationsPage.xaml | 8 +++
.../Settings/IntegrationsViewModel.cs | 6 ++
9 files changed, 185 insertions(+), 22 deletions(-)
create mode 100644 Bloxstrap/MultiInstanceWatcher.cs
diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs
index f680e00..af3e614 100644
--- a/Bloxstrap/Bootstrapper.cs
+++ b/Bloxstrap/Bootstrapper.cs
@@ -471,21 +471,48 @@ namespace Bloxstrap
}
}
+ private static void LaunchMultiInstanceWatcher()
+ {
+ const string LOG_IDENT = "Bootstrapper::LaunchMultiInstanceWatcher";
+
+ if (Utilities.DoesMutexExist("ROBLOX_singletonMutex"))
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Roblox singleton mutex already exists");
+ return;
+ }
+
+ using EventWaitHandle initEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-MultiInstanceWatcherInitialisationFinished");
+ Process.Start(Paths.Process, "-multiinstancewatcher");
+
+ bool initSuccess = initEventHandle.WaitOne(TimeSpan.FromSeconds(2));
+ if (initSuccess)
+ App.Logger.WriteLine(LOG_IDENT, "Initialisation finished signalled, continuing.");
+ else
+ App.Logger.WriteLine(LOG_IDENT, "Did not receive the initialisation finished signal, continuing.");
+ }
+
private void StartRoblox()
{
const string LOG_IDENT = "Bootstrapper::StartRoblox";
SetStatus(Strings.Bootstrapper_Status_Starting);
- if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage)
+ if (_launchMode == LaunchMode.Player)
{
- var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
+ // this needs to be done before roblox launches
+ if (App.Settings.Prop.MultiInstanceLaunching)
+ LaunchMultiInstanceWatcher();
- if (match.Groups.Count == 2)
- _launchCommandLine = _launchCommandLine.Replace(
- "robloxLocale:en_us",
- $"robloxLocale:{match.Groups[1].Value}",
- StringComparison.OrdinalIgnoreCase);
+ if (App.Settings.Prop.ForceRobloxLanguage)
+ {
+ var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
+
+ if (match.Groups.Count == 2)
+ _launchCommandLine = _launchCommandLine.Replace(
+ "robloxLocale:en_us",
+ $"robloxLocale:{match.Groups[1].Value}",
+ StringComparison.OrdinalIgnoreCase);
+ }
}
var startInfo = new ProcessStartInfo()
diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs
index 1eee8b2..4171b83 100644
--- a/Bloxstrap/LaunchHandler.cs
+++ b/Bloxstrap/LaunchHandler.cs
@@ -59,6 +59,11 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
LaunchWatcher();
}
+ else if (App.LaunchSettings.MultiInstanceWatcherFlag.Active)
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Opening multi-instance watcher");
+ LaunchMultiInstanceWatcher();
+ }
else if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Opening background updater");
@@ -223,7 +228,7 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
}
- if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
+ if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _) && !App.Settings.Prop.MultiInstanceLaunching)
{
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex
// which often hangs around for a few seconds after the window closes
@@ -302,6 +307,28 @@ namespace Bloxstrap
});
}
+ public static void LaunchMultiInstanceWatcher()
+ {
+ const string LOG_IDENT = "LaunchHandler::LaunchMultiInstanceWatcher";
+
+ App.Logger.WriteLine(LOG_IDENT, "Starting multi-instance watcher");
+
+ Task.Run(MultiInstanceWatcher.Run).ContinueWith(t =>
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Multi instance watcher task has finished");
+
+ if (t.IsFaulted)
+ {
+ App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the multi-instance watcher");
+
+ if (t.Exception is not null)
+ App.FinalizeExceptionHandling(t.Exception);
+ }
+
+ App.Terminate();
+ });
+ }
+
public static void LaunchBackgroundUpdater()
{
const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater";
diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs
index e0a5051..e7e8543 100644
--- a/Bloxstrap/LaunchSettings.cs
+++ b/Bloxstrap/LaunchSettings.cs
@@ -12,33 +12,35 @@ namespace Bloxstrap
{
public class LaunchSettings
{
- public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
+ public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
- public LaunchFlag WatcherFlag { get; } = new("watcher");
+ public LaunchFlag WatcherFlag { get; } = new("watcher");
- public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
+ public LaunchFlag MultiInstanceWatcherFlag { get; } = new("multiinstancewatcher");
- public LaunchFlag QuietFlag { get; } = new("quiet");
+ public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
- public LaunchFlag UninstallFlag { get; } = new("uninstall");
+ public LaunchFlag QuietFlag { get; } = new("quiet");
- public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
+ public LaunchFlag UninstallFlag { get; } = new("uninstall");
+
+ public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
- public LaunchFlag TestModeFlag { get; } = new("testmode");
+ public LaunchFlag TestModeFlag { get; } = new("testmode");
- public LaunchFlag NoGPUFlag { get; } = new("nogpu");
+ public LaunchFlag NoGPUFlag { get; } = new("nogpu");
- public LaunchFlag UpgradeFlag { get; } = new("upgrade");
+ public LaunchFlag UpgradeFlag { get; } = new("upgrade");
- public LaunchFlag PlayerFlag { get; } = new("player");
+ public LaunchFlag PlayerFlag { get; } = new("player");
- public LaunchFlag StudioFlag { get; } = new("studio");
+ public LaunchFlag StudioFlag { get; } = new("studio");
- public LaunchFlag VersionFlag { get; } = new("version");
+ public LaunchFlag VersionFlag { get; } = new("version");
- public LaunchFlag ChannelFlag { get; } = new("channel");
+ public LaunchFlag ChannelFlag { get; } = new("channel");
- public LaunchFlag ForceFlag { get; } = new("force");
+ public LaunchFlag ForceFlag { get; } = new("force");
#if DEBUG
public bool BypassUpdateCheck => true;
diff --git a/Bloxstrap/Models/Persistable/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs
index 0f6bb86..eb9c273 100644
--- a/Bloxstrap/Models/Persistable/Settings.cs
+++ b/Bloxstrap/Models/Persistable/Settings.cs
@@ -11,6 +11,7 @@ namespace Bloxstrap.Models.Persistable
public string BootstrapperIconCustomLocation { get; set; } = "";
public Theme Theme { get; set; } = Theme.Default;
public bool CheckForUpdates { get; set; } = true;
+ public bool MultiInstanceLaunching { get; set; } = false;
public bool ConfirmLaunches { get; set; } = false;
public string Locale { get; set; } = "nil";
public bool ForceRobloxLanguage { get; set; } = false;
diff --git a/Bloxstrap/MultiInstanceWatcher.cs b/Bloxstrap/MultiInstanceWatcher.cs
new file mode 100644
index 0000000..8799490
--- /dev/null
+++ b/Bloxstrap/MultiInstanceWatcher.cs
@@ -0,0 +1,68 @@
+namespace Bloxstrap
+{
+ internal static class MultiInstanceWatcher
+ {
+ private static int GetOpenProcessesCount()
+ {
+ const string LOG_IDENT = "MultiInstanceWatcher::GetOpenProcessesCount";
+
+ try
+ {
+ // prevent any possible race conditions by checking for bloxstrap processes too
+ int count = Process.GetProcesses().Count(x => x.ProcessName is "RobloxPlayerBeta" or "Bloxstrap");
+ count -= 1; // ignore the current process
+ return count;
+ }
+ catch (Exception ex)
+ {
+ // everything process related can error at any time
+ App.Logger.WriteException(LOG_IDENT, ex);
+ return -1;
+ }
+ }
+
+ private static void FireInitialisedEvent()
+ {
+ using EventWaitHandle initEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-MultiInstanceWatcherInitialisationFinished");
+ initEventHandle.Set();
+ }
+
+ public static void Run()
+ {
+ const string LOG_IDENT = "MultiInstanceWatcher::Run";
+
+ // try to get the mutex
+ bool acquiredMutex;
+ using Mutex mutex = new Mutex(false, "ROBLOX_singletonMutex");
+ try
+ {
+ acquiredMutex = mutex.WaitOne(0);
+ }
+ catch (AbandonedMutexException)
+ {
+ acquiredMutex = true;
+ }
+
+ if (!acquiredMutex)
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Client singleton mutex is already acquired");
+ FireInitialisedEvent();
+ return;
+ }
+
+ App.Logger.WriteLine(LOG_IDENT, "Acquired mutex!");
+ FireInitialisedEvent();
+
+ // watch for alive processes
+ int count;
+ do
+ {
+ Thread.Sleep(5000);
+ count = GetOpenProcessesCount();
+ }
+ while (count == -1 || count > 0); // redo if -1 (one of the Process apis failed)
+
+ App.Logger.WriteLine(LOG_IDENT, "All Roblox related processes have closed, exiting!");
+ }
+ }
+}
diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs
index 848b724..f61475b 100644
--- a/Bloxstrap/Resources/Strings.Designer.cs
+++ b/Bloxstrap/Resources/Strings.Designer.cs
@@ -3425,6 +3425,24 @@ namespace Bloxstrap.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Allows for having more than one Roblox game client instance open simultaneously..
+ ///
+ public static string Menu_Integrations_MultiInstanceLaunching_Description {
+ get {
+ return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Allow multi-instance launching.
+ ///
+ public static string Menu_Integrations_MultiInstanceLaunching_Title {
+ get {
+ return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Title", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to When in-game, you'll be able to see where your server is located via [ipinfo.io]({0})..
///
diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx
index f0355ae..2e9abaa 100644
--- a/Bloxstrap/Resources/Strings.resx
+++ b/Bloxstrap/Resources/Strings.resx
@@ -1487,4 +1487,10 @@ Defaulting to {1}.
Custom Theme {0}
{0} is a string (e.g. '1', '1-1234')
+
+ Allow multi-instance launching
+
+
+ Allows for having more than one Roblox game client instance open simultaneously.
+
\ No newline at end of file
diff --git a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml
index 0e391bb..0cb02e6 100644
--- a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml
+++ b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml
@@ -66,6 +66,14 @@
+
+
+
+
+
+
diff --git a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs
index d66789d..4aed82e 100644
--- a/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Settings/IntegrationsViewModel.cs
@@ -125,6 +125,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
set => App.Settings.Prop.UseDisableAppPatch = value;
}
+ public bool MultiInstanceLaunchingEnabled
+ {
+ get => App.Settings.Prop.MultiInstanceLaunching;
+ set => App.Settings.Prop.MultiInstanceLaunching = value;
+ }
+
public ObservableCollection CustomIntegrations
{
get => App.Settings.Prop.CustomIntegrations;
From afc3200b681d9c2ef817bdff051d7ee58af314ba Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 17:53:00 +0000
Subject: [PATCH 07/22] Improve LaunchSettings constructor (#4889)
* update log ident
* move flagMap inside the ctor function
---
Bloxstrap/LaunchSettings.cs | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs
index e7e8543..337e482 100644
--- a/Bloxstrap/LaunchSettings.cs
+++ b/Bloxstrap/LaunchSettings.cs
@@ -57,11 +57,9 @@ namespace Bloxstrap
///
public string[] Args { get; private set; }
- private readonly Dictionary _flagMap = new();
-
public LaunchSettings(string[] args)
{
- const string LOG_IDENT = "LaunchSettings";
+ const string LOG_IDENT = "LaunchSettings::LaunchSettings";
#if DEBUG
App.Logger.WriteLine(LOG_IDENT, $"Launched with arguments: {string.Join(' ', args)}");
@@ -69,6 +67,8 @@ namespace Bloxstrap
Args = args;
+ Dictionary flagMap = new();
+
// build flag map
foreach (var prop in this.GetType().GetProperties())
{
@@ -79,7 +79,7 @@ namespace Bloxstrap
continue;
foreach (string identifier in flag.Identifiers.Split(','))
- _flagMap.Add(identifier, flag);
+ flagMap.Add(identifier, flag);
}
int startIdx = 0;
@@ -119,7 +119,7 @@ namespace Bloxstrap
string identifier = arg[1..];
- if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
+ if (!flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
{
App.Logger.WriteLine(LOG_IDENT, $"Unknown argument: {identifier}");
continue;
From 3f02c6ba93ec2f24f020417506ff2542f086187e Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 17:55:13 +0000
Subject: [PATCH 08/22] Reset `ForceReinstall` after upgrade (#4890)
---
Bloxstrap/Bootstrapper.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs
index af3e614..d1889d4 100644
--- a/Bloxstrap/Bootstrapper.cs
+++ b/Bloxstrap/Bootstrapper.cs
@@ -1136,6 +1136,8 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
+ App.State.Prop.ForceReinstall = false;
+
App.State.Save();
App.RobloxState.Save();
From bae578f94df4fde637503e4047c62f71f2daa919 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sat, 15 Mar 2025 17:55:42 +0000
Subject: [PATCH 09/22] Update wpfui
---
wpfui | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/wpfui b/wpfui
index dca423b..f710123 160000
--- a/wpfui
+++ b/wpfui
@@ -1 +1 @@
-Subproject commit dca423b724ec24bd3377da3a27f4055ae317b50a
+Subproject commit f710123e72d9dcc8d09fccc4e2a783cc5cf5e652
From 950277d1e9a45c451a86baf566a12930d2fd75f2 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sun, 16 Mar 2025 02:12:41 +0000
Subject: [PATCH 10/22] fix crash on theme create
---
Bloxstrap/Extensions/CustomThemeTemplateEx.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Bloxstrap/Extensions/CustomThemeTemplateEx.cs b/Bloxstrap/Extensions/CustomThemeTemplateEx.cs
index ed9dcea..4b72197 100644
--- a/Bloxstrap/Extensions/CustomThemeTemplateEx.cs
+++ b/Bloxstrap/Extensions/CustomThemeTemplateEx.cs
@@ -20,12 +20,12 @@ namespace Bloxstrap.Extensions
case CustomThemeTemplate.Blank:
{
string moreText = string.Format(Strings.CustomTheme_Templates_Blank_MoreExamples, EXAMPLES_URL);
- return string.Format(contents, Strings.CustomTheme_Templates_Blank_UIElements, moreText);
+ return contents.Replace("{0}", Strings.CustomTheme_Templates_Blank_UIElements).Replace("{1}", moreText);
}
case CustomThemeTemplate.Simple:
{
string moreText = string.Format(Strings.CustomTheme_Templates_Simple_MoreExamples, EXAMPLES_URL);
- return string.Format(contents, moreText);
+ return contents.Replace("{0}", moreText);
}
default:
Debug.Assert(false);
From 4d50381115e889c9122502435e23f4ce087fe8de Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sun, 16 Mar 2025 02:15:57 +0000
Subject: [PATCH 11/22] fix string not being localised
---
Bloxstrap/Resources/Strings.Designer.cs | 9 +++++++++
Bloxstrap/Resources/Strings.resx | 3 +++
.../Editor/BootstrapperEditorWindowViewModel.cs | 2 +-
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs
index f61475b..fb38f4f 100644
--- a/Bloxstrap/Resources/Strings.Designer.cs
+++ b/Bloxstrap/Resources/Strings.Designer.cs
@@ -1036,6 +1036,15 @@ namespace Bloxstrap.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Your theme has been saved!.
+ ///
+ public static string CustomTheme_Editor_Save_Success_Description {
+ get {
+ return ResourceManager.GetString("CustomTheme.Editor.Save.Success.Description", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Editing "{0}".
///
diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx
index 2e9abaa..e4111a7 100644
--- a/Bloxstrap/Resources/Strings.resx
+++ b/Bloxstrap/Resources/Strings.resx
@@ -1493,4 +1493,7 @@ Defaulting to {1}.
Allows for having more than one Roblox game client instance open simultaneously.
+
+ Your theme has been saved!
+
\ No newline at end of file
diff --git a/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs b/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs
index 2e0bcb4..b8c1968 100644
--- a/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs
@@ -64,7 +64,7 @@ namespace Bloxstrap.UI.ViewModels.Editor
{
File.WriteAllText(path, Code);
CodeChanged = false;
- ThemeSavedCallback.Invoke(true, "Your theme has been saved!");
+ ThemeSavedCallback.Invoke(true, Strings.CustomTheme_Editor_Save_Success_Description);
}
catch (Exception ex)
{
From a037c9f867839574f980b5a6c1b2a8e04144343e Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sun, 16 Mar 2025 11:18:23 +0000
Subject: [PATCH 12/22] update localisation
---
Bloxstrap/Resources/Strings.Designer.cs | 15 ++++++++++++---
Bloxstrap/Resources/Strings.resx | 9 ++++++---
.../Bootstrapper/CustomDialog.Elements.cs | 2 +-
.../Editor/BootstrapperEditorWindowViewModel.cs | 2 +-
4 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs
index fb38f4f..547f49a 100644
--- a/Bloxstrap/Resources/Strings.Designer.cs
+++ b/Bloxstrap/Resources/Strings.Designer.cs
@@ -188,7 +188,7 @@ namespace Bloxstrap.Resources {
}
///
- /// Looks up a localized string similar to Failed to extract all files.
+ /// Looks up a localized string similar to Failed to extract files.
///
public static string Bootstrapper_ExtractionFailed_Title {
get {
@@ -235,7 +235,7 @@ namespace Bloxstrap.Resources {
}
///
- /// Looks up a localized string similar to Failed to apply all modifications.
+ /// Looks up a localized string similar to Failed to apply modifications.
///
public static string Bootstrapper_ModificationsFailed_Title {
get {
@@ -991,6 +991,15 @@ namespace Bloxstrap.Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Failed to preview theme: {0}.
+ ///
+ public static string CustomTheme_Editor_Errors_PreviewFailed {
+ get {
+ return ResourceManager.GetString("CustomTheme.Editor.Errors.PreviewFailed", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Open Theme Directory.
///
@@ -1218,7 +1227,7 @@ namespace Bloxstrap.Resources {
}
///
- /// Looks up a localized string similar to Custom bootstrappers can only have a maximum of {0} elements, got {1}..
+ /// Looks up a localized string similar to Custom bootstrappers can only have a maximum of {0} elements, got {1}.
///
public static string CustomTheme_Errors_TooManyElements {
get {
diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx
index e4111a7..33a9d19 100644
--- a/Bloxstrap/Resources/Strings.resx
+++ b/Bloxstrap/Resources/Strings.resx
@@ -1274,13 +1274,13 @@ Please close any applications that may be using Roblox's files, and relaunch.Roblox no longer supports Windows 7 or 8.1. To continue playing Roblox, please upgrade to Windows 10 or newer.
- Failed to extract all files
+ Failed to extract files
Some content may be missing. Force a Roblox reinstallation in settings to fix this.
- Failed to apply all modifications
+ Failed to apply modifications
Not all modifications will be present in the current launch.
@@ -1302,7 +1302,7 @@ Please close any applications that may be using Roblox's files, and relaunch.Custom dialog has already been initialised
- Custom bootstrappers can only have a maximum of {0} elements, got {1}.
+ Custom bootstrappers can only have a maximum of {0} elements, got {1}
{0} and {1} are numbers
@@ -1496,4 +1496,7 @@ Defaulting to {1}.
Your theme has been saved!
+
+ Failed to preview theme: {0}
+
\ No newline at end of file
diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs
index 68a61d0..33743c6 100644
--- a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs
+++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs
@@ -416,7 +416,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper_Fake(CustomDialog dialog, XElement xmlElement)
{
// this only exists to error out the theme if someone tries to use two BloxstrapCustomBootstrappers
- throw new Exception($"{xmlElement.Parent!.Name} cannot have a child of {xmlElement.Name}");
+ throw new CustomThemeException("CustomTheme.Errors.ElementInvalidChild", xmlElement.Parent!.Name, xmlElement.Name);
}
private static DummyFrameworkElement HandleXmlElement_TitleBar(CustomDialog dialog, XElement xmlElement)
diff --git a/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs b/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs
index b8c1968..72129b8 100644
--- a/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Editor/BootstrapperEditorWindowViewModel.cs
@@ -50,7 +50,7 @@ namespace Bloxstrap.UI.ViewModels.Editor
App.Logger.WriteLine(LOG_IDENT, "Failed to preview custom theme");
App.Logger.WriteException(LOG_IDENT, ex);
- Frontend.ShowMessageBox($"Failed to preview theme: {ex.Message}", MessageBoxImage.Error, MessageBoxButton.OK);
+ Frontend.ShowMessageBox(string.Format(Strings.CustomTheme_Editor_Errors_PreviewFailed, ex.Message), MessageBoxImage.Error, MessageBoxButton.OK);
}
}
From 40ed053ebe7651c6d626ad51e5313e90340aa449 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Sun, 16 Mar 2025 11:19:25 +0000
Subject: [PATCH 13/22] dont show error balloon tips if quiet
---
Bloxstrap/Bootstrapper.cs | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs
index d1889d4..5578fe2 100644
--- a/Bloxstrap/Bootstrapper.cs
+++ b/Bloxstrap/Bootstrapper.cs
@@ -294,11 +294,14 @@ namespace Bloxstrap
if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested)
{
- // show some balloon tips
- if (!_packageExtractionSuccess)
- Frontend.ShowBalloonTip(Strings.Bootstrapper_ExtractionFailed_Title, Strings.Bootstrapper_ExtractionFailed_Message, ToolTipIcon.Warning);
- else if (!allModificationsApplied)
- Frontend.ShowBalloonTip(Strings.Bootstrapper_ModificationsFailed_Title, Strings.Bootstrapper_ModificationsFailed_Message, ToolTipIcon.Warning);
+ if (!App.LaunchSettings.QuietFlag.Active)
+ {
+ // show some balloon tips
+ if (!_packageExtractionSuccess)
+ Frontend.ShowBalloonTip(Strings.Bootstrapper_ExtractionFailed_Title, Strings.Bootstrapper_ExtractionFailed_Message, ToolTipIcon.Warning);
+ else if (!allModificationsApplied)
+ Frontend.ShowBalloonTip(Strings.Bootstrapper_ModificationsFailed_Title, Strings.Bootstrapper_ModificationsFailed_Message, ToolTipIcon.Warning);
+ }
StartRoblox();
}
From ba799962cc721b0641d98db70650e2f46c4ca607 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Mon, 17 Mar 2025 18:14:23 +0000
Subject: [PATCH 14/22] add code signing policy to readme
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 4f31fc6..8a8be4c 100644
--- a/README.md
+++ b/README.md
@@ -83,3 +83,7 @@ Bloxstrap uses the [WPF UI](https://github.com/lepoco/wpfui) library for the use
[crowdin-project]: https://crowdin.com/project/bloxstrap
[discord-invite]: https://discord.gg/nKjV3mGq6R
[tenor-gif]: https://media.tenor.com/FIkSGbGycmAAAAAd/manly-roblox.gif
+
+## Code signing policy
+
+Thanks to [SignPath.io](https://signpath.io/) for providing a free code signing service, and the [SignPath Foundation](https://signpath.org/) for providing the free code signing certificate.
From 8e81983feef9b2905f7bd51996b803f8dd6f80a9 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Tue, 18 Mar 2025 10:12:46 +0000
Subject: [PATCH 15/22] fix crash in CleanupVersionsFolder
---
Bloxstrap/Bootstrapper.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs
index 5578fe2..bfde105 100644
--- a/Bloxstrap/Bootstrapper.cs
+++ b/Bloxstrap/Bootstrapper.cs
@@ -859,6 +859,12 @@ namespace Bloxstrap
return;
}
+ if (!Directory.Exists(Paths.Versions))
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Versions directory does not exist, skipping cleanup.");
+ return;
+ }
+
foreach (string dir in Directory.GetDirectories(Paths.Versions))
{
string dirName = Path.GetFileName(dir);
From 12bc3ef6e70a691c03a39dbe92facc7fb0900572 Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Thu, 20 Mar 2025 16:39:02 +0000
Subject: [PATCH 16/22] Web environments (#4911)
* add web environments
* add comment
* update enum name
* fixes and improvements
* add new enum value
* update enum names
---
Bloxstrap/App.xaml.cs | 42 +++++++++++++++++--
Bloxstrap/Enums/WebEnvironment.cs | 26 ++++++++++++
Bloxstrap/Extensions/TEnumEx.cs | 22 ++++++++++
Bloxstrap/JsonManager.cs | 3 ++
Bloxstrap/Models/Persistable/Settings.cs | 3 ++
.../Settings/Pages/BloxstrapPage.xaml | 8 ++++
.../ViewModels/Settings/BloxstrapViewModel.cs | 13 +++++-
7 files changed, 113 insertions(+), 4 deletions(-)
create mode 100644 Bloxstrap/Enums/WebEnvironment.cs
create mode 100644 Bloxstrap/Extensions/TEnumEx.cs
diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs
index dfc11ca..81cecce 100644
--- a/Bloxstrap/App.xaml.cs
+++ b/Bloxstrap/App.xaml.cs
@@ -65,6 +65,20 @@ namespace Bloxstrap
);
private static bool _showingExceptionDialog = false;
+
+ private static string? _webUrl = null;
+ public static string WebUrl
+ {
+ get {
+ if (_webUrl != null)
+ return _webUrl;
+
+ string url = ConstructBloxstrapWebUrl();
+ if (Settings.Loaded) // only cache if settings are done loading
+ _webUrl = url;
+ return url;
+ }
+ }
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
@@ -126,6 +140,25 @@ namespace Bloxstrap
Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
}
+ public static string ConstructBloxstrapWebUrl()
+ {
+ // dont let user switch web environment if debug mode is not on
+ if (Settings.Prop.WebEnvironment == WebEnvironment.Production || !Settings.Prop.DeveloperMode)
+ return "bloxstraplabs.com";
+
+ string? sub = Settings.Prop.WebEnvironment.GetDescription();
+ return $"web-{sub}.bloxstraplabs.com";
+ }
+
+ public static bool CanSendLogs()
+ {
+ // non developer mode always uses production
+ if (!Settings.Prop.DeveloperMode || Settings.Prop.WebEnvironment == WebEnvironment.Production)
+ return IsProductionBuild;
+
+ return true;
+ }
+
public static async Task GetLatestRelease()
{
const string LOG_IDENT = "App::GetLatestRelease";
@@ -157,7 +190,7 @@ namespace Bloxstrap
try
{
- await HttpClient.GetAsync($"https://bloxstraplabs.com/metrics/post?key={key}&value={value}");
+ await HttpClient.GetAsync($"https://{WebUrl}/metrics/post?key={key}&value={value}");
}
catch (Exception ex)
{
@@ -167,13 +200,13 @@ namespace Bloxstrap
public static async void SendLog()
{
- if (!Settings.Prop.EnableAnalytics || !IsProductionBuild)
+ if (!Settings.Prop.EnableAnalytics || !CanSendLogs())
return;
try
{
await HttpClient.PostAsync(
- $"https://bloxstraplabs.com/metrics/post-exception",
+ $"https://{WebUrl}/metrics/post-exception",
new StringContent(Logger.AsDocument)
);
}
@@ -347,6 +380,9 @@ namespace Bloxstrap
Settings.Save();
}
+ Logger.WriteLine(LOG_IDENT, $"Developer mode: {Settings.Prop.DeveloperMode}");
+ Logger.WriteLine(LOG_IDENT, $"Web environment: {Settings.Prop.WebEnvironment}");
+
Locale.Set(Settings.Prop.Locale);
if (!LaunchSettings.BypassUpdateCheck)
diff --git a/Bloxstrap/Enums/WebEnvironment.cs b/Bloxstrap/Enums/WebEnvironment.cs
new file mode 100644
index 0000000..5bf857a
--- /dev/null
+++ b/Bloxstrap/Enums/WebEnvironment.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel;
+
+namespace Bloxstrap.Enums
+{
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum WebEnvironment
+ {
+ [Description("prod")]
+ Production,
+
+ [Description("stage")]
+ Staging,
+
+ [Description("dev")]
+ Dev,
+
+ [Description("pizza")]
+ DevPizza,
+
+ [Description("matt")]
+ DevMatt,
+
+ [Description("local")]
+ Local
+ }
+}
diff --git a/Bloxstrap/Extensions/TEnumEx.cs b/Bloxstrap/Extensions/TEnumEx.cs
new file mode 100644
index 0000000..ddefb8f
--- /dev/null
+++ b/Bloxstrap/Extensions/TEnumEx.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel;
+using System.Reflection;
+
+namespace Bloxstrap.Extensions
+{
+ internal static class TEnumEx
+ {
+ public static string? GetDescription(this TEnum e)
+ {
+ string? enumName = e.ToString();
+ if (enumName == null)
+ return null;
+
+ FieldInfo? field = e.GetType().GetField(enumName);
+ if (field == null)
+ return null;
+
+ DescriptionAttribute? attribute = field.GetCustomAttribute();
+ return attribute?.Description;
+ }
+ }
+}
diff --git a/Bloxstrap/JsonManager.cs b/Bloxstrap/JsonManager.cs
index 6c04a2a..2bf55cf 100644
--- a/Bloxstrap/JsonManager.cs
+++ b/Bloxstrap/JsonManager.cs
@@ -13,6 +13,8 @@ namespace Bloxstrap
///
public string? LastFileHash { get; private set; }
+ public bool Loaded { get; set; } = false;
+
public virtual string ClassName => typeof(T).Name;
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
@@ -35,6 +37,7 @@ namespace Bloxstrap
throw new ArgumentNullException("Deserialization returned null");
Prop = settings;
+ Loaded = true;
LastFileHash = MD5Hash.FromString(contents);
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
diff --git a/Bloxstrap/Models/Persistable/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs
index eb9c273..13f834c 100644
--- a/Bloxstrap/Models/Persistable/Settings.cs
+++ b/Bloxstrap/Models/Persistable/Settings.cs
@@ -10,6 +10,8 @@ namespace Bloxstrap.Models.Persistable
public string BootstrapperTitle { get; set; } = App.ProjectName;
public string BootstrapperIconCustomLocation { get; set; } = "";
public Theme Theme { get; set; } = Theme.Default;
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool DeveloperMode { get; set; } = false;
public bool CheckForUpdates { get; set; } = true;
public bool MultiInstanceLaunching { get; set; } = false;
public bool ConfirmLaunches { get; set; } = false;
@@ -21,6 +23,7 @@ namespace Bloxstrap.Models.Persistable
public bool BackgroundUpdatesEnabled { get; set; } = true;
public bool DebugDisableVersionPackageCleanup { get; set; } = false;
public string? SelectedCustomTheme { get; set; } = null;
+ public WebEnvironment WebEnvironment { get; set; } = WebEnvironment.Production;
// integration configuration
public bool EnableActivityTracking { get; set; } = true;
diff --git a/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml
index d4f731a..326f859 100644
--- a/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml
+++ b/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml
@@ -29,6 +29,14 @@
+
+
+
+
+
diff --git a/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs
index 63ce62f..b1eb626 100644
--- a/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs
+++ b/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs
@@ -1,4 +1,5 @@
-using System.Windows.Input;
+using System.Windows;
+using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Win32;
@@ -7,6 +8,8 @@ namespace Bloxstrap.UI.ViewModels.Settings
{
public class BloxstrapViewModel : NotifyPropertyChangedViewModel
{
+ public WebEnvironment[] WebEnvironments => Enum.GetValues();
+
public bool UpdateCheckingEnabled
{
get => App.Settings.Prop.CheckForUpdates;
@@ -19,6 +22,14 @@ namespace Bloxstrap.UI.ViewModels.Settings
set => App.Settings.Prop.EnableAnalytics = value;
}
+ public WebEnvironment WebEnvironment
+ {
+ get => App.Settings.Prop.WebEnvironment;
+ set => App.Settings.Prop.WebEnvironment = value;
+ }
+
+ public Visibility WebEnvironmentVisibility => App.Settings.Prop.DeveloperMode ? Visibility.Visible : Visibility.Collapsed;
+
public bool ShouldExportConfig { get; set; } = true;
public bool ShouldExportLogs { get; set; } = true;
From f75d755e9e434f16dc13c4bb17f4e309ea1afc86 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Thu, 20 Mar 2025 16:40:09 +0000
Subject: [PATCH 17/22] fix build warnings
---
Bloxstrap/Extensions/TEnumEx.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Bloxstrap/Extensions/TEnumEx.cs b/Bloxstrap/Extensions/TEnumEx.cs
index ddefb8f..971334d 100644
--- a/Bloxstrap/Extensions/TEnumEx.cs
+++ b/Bloxstrap/Extensions/TEnumEx.cs
@@ -7,11 +7,11 @@ namespace Bloxstrap.Extensions
{
public static string? GetDescription(this TEnum e)
{
- string? enumName = e.ToString();
+ string? enumName = e?.ToString();
if (enumName == null)
return null;
- FieldInfo? field = e.GetType().GetField(enumName);
+ FieldInfo? field = e?.GetType().GetField(enumName);
if (field == null)
return null;
From 4dcb72b27f24cbba7a32695bfc8a9f6e1e351214 Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Tue, 25 Mar 2025 09:02:44 +0000
Subject: [PATCH 18/22] Add help link to web environments (#4934)
---
Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml
index 326f859..75d050e 100644
--- a/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml
+++ b/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml
@@ -33,7 +33,8 @@
+ Description="Site to use for metrics"
+ HelpLink="https://admin.bloxstraplabs.com/Wiki/Developers/Web-Environments">
From 0f13750d7cb65c3c802b96a8adb84d25b70ebecc Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Tue, 25 Mar 2025 09:05:17 +0000
Subject: [PATCH 19/22] make background updates opt-in
---
Bloxstrap/Models/Persistable/Settings.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Bloxstrap/Models/Persistable/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs
index 13f834c..eebf03d 100644
--- a/Bloxstrap/Models/Persistable/Settings.cs
+++ b/Bloxstrap/Models/Persistable/Settings.cs
@@ -20,7 +20,7 @@ namespace Bloxstrap.Models.Persistable
public bool UseFastFlagManager { get; set; } = true;
public bool WPFSoftwareRender { get; set; } = false;
public bool EnableAnalytics { get; set; } = true;
- public bool BackgroundUpdatesEnabled { get; set; } = true;
+ public bool BackgroundUpdatesEnabled { get; set; } = false;
public bool DebugDisableVersionPackageCleanup { get; set; } = false;
public string? SelectedCustomTheme { get; set; } = null;
public WebEnvironment WebEnvironment { get; set; } = WebEnvironment.Production;
From 18ca67df274b1f0328f3ae2d12decd50cca1f373 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Tue, 25 Mar 2025 09:06:36 +0000
Subject: [PATCH 20/22] update name (#4906)
---
Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml b/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml
index 486cd12..2b6d6ed 100644
--- a/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml
+++ b/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml
@@ -127,7 +127,7 @@
-
+
From 055695e014262afea29dd83ee45f39d4ff4c4183 Mon Sep 17 00:00:00 2001
From: bluepilledgreat <97983689+bluepilledgreat@users.noreply.github.com>
Date: Tue, 25 Mar 2025 09:10:45 +0000
Subject: [PATCH 21/22] update cancel button text in the simple template
---
Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml b/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml
index abe586d..bb3b1d1 100644
--- a/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml
+++ b/Bloxstrap/Resources/CustomBootstrapperTemplate_Simple.xml
@@ -5,5 +5,5 @@
-
+
\ No newline at end of file
From c1842c044346bee6448eef06bc1ea04cda17cf49 Mon Sep 17 00:00:00 2001
From: Matt <97983689+bluepilledgreat@users.noreply.github.com>
Date: Fri, 28 Mar 2025 19:33:51 +0000
Subject: [PATCH 22/22] Replace AssetDelivery API with Thumbnails API for
Discord RPC images (#4947)
* replace assetdelivery with thumbnails for rpc
* update GetThumbnailUrlAsync logging
* fix build error
---
Bloxstrap/Extensions/HttpClientEx.cs | 81 +++++
Bloxstrap/Integrations/DiscordRichPresence.cs | 326 +++++++++++++-----
.../APIs/Roblox/ThumbnailBatchResponse.cs | 8 +
.../Models/APIs/Roblox/ThumbnailRequest.cs | 34 ++
.../Models/APIs/Roblox/ThumbnailResponse.cs | 20 +-
Bloxstrap/Models/ThumbnailCacheEntry.cs | 8 +
Bloxstrap/Utility/FixedCapacityList.cs | 25 ++
Bloxstrap/Utility/Thumbnails.cs | 103 ++++++
8 files changed, 513 insertions(+), 92 deletions(-)
create mode 100644 Bloxstrap/Extensions/HttpClientEx.cs
create mode 100644 Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
create mode 100644 Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
create mode 100644 Bloxstrap/Models/ThumbnailCacheEntry.cs
create mode 100644 Bloxstrap/Utility/FixedCapacityList.cs
create mode 100644 Bloxstrap/Utility/Thumbnails.cs
diff --git a/Bloxstrap/Extensions/HttpClientEx.cs b/Bloxstrap/Extensions/HttpClientEx.cs
new file mode 100644
index 0000000..00680fa
--- /dev/null
+++ b/Bloxstrap/Extensions/HttpClientEx.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Bloxstrap.Extensions
+{
+ internal static class HttpClientEx
+ {
+ public static async Task GetWithRetriesAsync(this HttpClient client, string url, int retries, CancellationToken token)
+ {
+ HttpResponseMessage response = null!;
+
+ for (int i = 1; i <= retries; i++)
+ {
+ try
+ {
+ response = await client.GetAsync(url, token);
+ }
+ catch (TaskCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ App.Logger.WriteException("HttpClientEx::GetWithRetriesAsync", ex);
+
+ if (i == retries)
+ throw;
+ }
+ }
+
+ return response;
+ }
+
+ public static async Task PostWithRetriesAsync(this HttpClient client, string url, HttpContent? content, int retries, CancellationToken token)
+ {
+ HttpResponseMessage response = null!;
+
+ for (int i = 1; i <= retries; i++)
+ {
+ try
+ {
+ response = await client.PostAsync(url, content, token);
+ }
+ catch (TaskCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ App.Logger.WriteException("HttpClientEx::PostWithRetriesAsync", ex);
+
+ if (i == retries)
+ throw;
+ }
+ }
+
+ return response;
+ }
+
+ public static async Task GetFromJsonWithRetriesAsync(this HttpClient client, string url, int retries, CancellationToken token) where T : class
+ {
+ HttpResponseMessage response = await GetWithRetriesAsync(client, url, retries, token);
+ response.EnsureSuccessStatusCode();
+
+ using var stream = await response.Content.ReadAsStreamAsync(token);
+ return await JsonSerializer.DeserializeAsync(stream, cancellationToken: token);
+ }
+
+ public static async Task PostFromJsonWithRetriesAsync(this HttpClient client, string url, HttpContent? content, int retries, CancellationToken token) where T : class
+ {
+ HttpResponseMessage response = await PostWithRetriesAsync(client, url, content, retries, token);
+ response.EnsureSuccessStatusCode();
+
+ using var stream = await response.Content.ReadAsStreamAsync(token);
+ return await JsonSerializer.DeserializeAsync(stream, cancellationToken: token);
+ }
+ }
+}
diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs
index 8d140f9..744c085 100644
--- a/Bloxstrap/Integrations/DiscordRichPresence.cs
+++ b/Bloxstrap/Integrations/DiscordRichPresence.cs
@@ -13,6 +13,12 @@ namespace Bloxstrap.Integrations
private DiscordRPC.RichPresence? _currentPresence;
private DiscordRPC.RichPresence? _originalPresence;
+ private FixedSizeList _thumbnailCache = new FixedSizeList(20);
+
+ private ulong? _smallImgBeingFetched = null;
+ private ulong? _largeImgBeingFetched = null;
+ private CancellationTokenSource? _fetchThumbnailsToken;
+
private bool _visible = true;
public DiscordRichPresence(ActivityWatcher activityWatcher)
@@ -69,101 +75,239 @@ namespace Bloxstrap.Integrations
}
else if (message.Command == "SetRichPresence")
{
- Models.BloxstrapRPC.RichPresence? presenceData;
-
- try
- {
- presenceData = message.Data.Deserialize();
- }
- catch (Exception)
- {
- App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
- return;
- }
-
- if (presenceData is null)
- {
- App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
- return;
- }
-
- if (presenceData.Details is not null)
- {
- if (presenceData.Details.Length > 128)
- App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
- else if (presenceData.Details == "")
- _currentPresence.Details = _originalPresence.Details;
- else
- _currentPresence.Details = presenceData.Details;
- }
-
- if (presenceData.State is not null)
- {
- if (presenceData.State.Length > 128)
- App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
- else if (presenceData.State == "")
- _currentPresence.State = _originalPresence.State;
- else
- _currentPresence.State = presenceData.State;
- }
-
- if (presenceData.TimestampStart == 0)
- _currentPresence.Timestamps.Start = null;
- else if (presenceData.TimestampStart is not null)
- _currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
-
- if (presenceData.TimestampEnd == 0)
- _currentPresence.Timestamps.End = null;
- else if (presenceData.TimestampEnd is not null)
- _currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
-
- if (presenceData.SmallImage is not null)
- {
- if (presenceData.SmallImage.Clear)
- {
- _currentPresence.Assets.SmallImageKey = "";
- }
- else if (presenceData.SmallImage.Reset)
- {
- _currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
- _currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
- }
- else
- {
- if (presenceData.SmallImage.AssetId is not null)
- _currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
-
- if (presenceData.SmallImage.HoverText is not null)
- _currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
- }
- }
-
- if (presenceData.LargeImage is not null)
- {
- if (presenceData.LargeImage.Clear)
- {
- _currentPresence.Assets.LargeImageKey = "";
- }
- else if (presenceData.LargeImage.Reset)
- {
- _currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
- _currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
- }
- else
- {
- if (presenceData.LargeImage.AssetId is not null)
- _currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
-
- if (presenceData.LargeImage.HoverText is not null)
- _currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
- }
- }
+ ProcessSetRichPresence(message, implicitUpdate);
}
if (implicitUpdate)
UpdatePresence();
}
+ private void AddToThumbnailCache(ulong id, string? url)
+ {
+ if (url != null)
+ _thumbnailCache.Add(new ThumbnailCacheEntry { Id = id, Url = url });
+ }
+
+ private async Task UpdatePresenceIconsAsync(ulong? smallImg, ulong? largeImg, bool implicitUpdate, CancellationToken token)
+ {
+ Debug.Assert(smallImg != null || largeImg != null);
+
+ if (smallImg != null && largeImg != null)
+ {
+ string?[] urls = await Thumbnails.GetThumbnailUrlsAsync(new List
+ {
+ new ThumbnailRequest
+ {
+ TargetId = (ulong)smallImg,
+ Type = "Asset",
+ Size = "512x512",
+ IsCircular = false
+ },
+ new ThumbnailRequest
+ {
+ TargetId = (ulong)largeImg,
+ Type = "Asset",
+ Size = "512x512",
+ IsCircular = false
+ }
+ }, token);
+
+ string? smallUrl = urls[0];
+ string? largeUrl = urls[1];
+
+ AddToThumbnailCache((ulong)smallImg, smallUrl);
+ AddToThumbnailCache((ulong)largeImg, largeUrl);
+
+ if (_currentPresence != null)
+ {
+ _currentPresence.Assets.SmallImageKey = smallUrl;
+ _currentPresence.Assets.LargeImageKey = largeUrl;
+ }
+ }
+ else if (smallImg != null)
+ {
+ string? url = await Thumbnails.GetThumbnailUrlAsync(new ThumbnailRequest
+ {
+ TargetId = (ulong)smallImg,
+ Type = "Asset",
+ Size = "512x512",
+ IsCircular = false
+ }, token);
+
+ AddToThumbnailCache((ulong)smallImg, url);
+
+ if (_currentPresence != null)
+ _currentPresence.Assets.SmallImageKey = url;
+ }
+ else if (largeImg != null)
+ {
+ string? url = await Thumbnails.GetThumbnailUrlAsync(new ThumbnailRequest
+ {
+ TargetId = (ulong)largeImg,
+ Type = "Asset",
+ Size = "512x512",
+ IsCircular = false
+ }, token);
+
+ AddToThumbnailCache((ulong)largeImg, url);
+
+ if (_currentPresence != null)
+ _currentPresence.Assets.LargeImageKey = url;
+ }
+
+ _smallImgBeingFetched = null;
+ _largeImgBeingFetched = null;
+
+ if (implicitUpdate)
+ UpdatePresence();
+ }
+
+ private void ProcessSetRichPresence(Message message, bool implicitUpdate)
+ {
+ const string LOG_IDENT = "DiscordRichPresence::ProcessSetRichPresence";
+ Models.BloxstrapRPC.RichPresence? presenceData;
+
+ Debug.Assert(_currentPresence is not null);
+ Debug.Assert(_originalPresence is not null);
+
+ if (_fetchThumbnailsToken != null)
+ {
+ _fetchThumbnailsToken.Cancel();
+ _fetchThumbnailsToken = null;
+ }
+
+ try
+ {
+ presenceData = message.Data.Deserialize();
+ }
+ catch (Exception)
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
+ return;
+ }
+
+ if (presenceData is null)
+ {
+ App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
+ return;
+ }
+
+ if (presenceData.Details is not null)
+ {
+ if (presenceData.Details.Length > 128)
+ App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
+ else if (presenceData.Details == "")
+ _currentPresence.Details = _originalPresence.Details;
+ else
+ _currentPresence.Details = presenceData.Details;
+ }
+
+ if (presenceData.State is not null)
+ {
+ if (presenceData.State.Length > 128)
+ App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
+ else if (presenceData.State == "")
+ _currentPresence.State = _originalPresence.State;
+ else
+ _currentPresence.State = presenceData.State;
+ }
+
+ if (presenceData.TimestampStart == 0)
+ _currentPresence.Timestamps.Start = null;
+ else if (presenceData.TimestampStart is not null)
+ _currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
+
+ if (presenceData.TimestampEnd == 0)
+ _currentPresence.Timestamps.End = null;
+ else if (presenceData.TimestampEnd is not null)
+ _currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
+
+ // set these to start fetching
+ ulong? smallImgFetch = null;
+ ulong? largeImgFetch = null;
+
+ if (presenceData.SmallImage is not null)
+ {
+ if (presenceData.SmallImage.Clear)
+ {
+ _currentPresence.Assets.SmallImageKey = "";
+ _smallImgBeingFetched = null;
+ }
+ else if (presenceData.SmallImage.Reset)
+ {
+ _currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
+ _currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
+ _smallImgBeingFetched = null;
+ }
+ else
+ {
+ if (presenceData.SmallImage.AssetId is not null)
+ {
+ ThumbnailCacheEntry? entry = _thumbnailCache.FirstOrDefault(x => x.Id == presenceData.SmallImage.AssetId);
+
+ if (entry == null)
+ {
+ smallImgFetch = presenceData.SmallImage.AssetId;
+ }
+ else
+ {
+ _currentPresence.Assets.SmallImageKey = entry.Url;
+ _smallImgBeingFetched = null;
+ }
+ }
+
+ if (presenceData.SmallImage.HoverText is not null)
+ _currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
+ }
+ }
+
+ if (presenceData.LargeImage is not null)
+ {
+ if (presenceData.LargeImage.Clear)
+ {
+ _currentPresence.Assets.LargeImageKey = "";
+ _largeImgBeingFetched = null;
+ }
+ else if (presenceData.LargeImage.Reset)
+ {
+ _currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
+ _currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
+ _largeImgBeingFetched = null;
+ }
+ else
+ {
+ if (presenceData.LargeImage.AssetId is not null)
+ {
+ ThumbnailCacheEntry? entry = _thumbnailCache.FirstOrDefault(x => x.Id == presenceData.LargeImage.AssetId);
+
+ if (entry == null)
+ {
+ largeImgFetch = presenceData.LargeImage.AssetId;
+ }
+ else
+ {
+ _currentPresence.Assets.LargeImageKey = entry.Url;
+ _largeImgBeingFetched = null;
+ }
+ }
+
+ if (presenceData.LargeImage.HoverText is not null)
+ _currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
+ }
+ }
+
+ if (smallImgFetch != null)
+ _smallImgBeingFetched = smallImgFetch;
+ if (largeImgFetch != null)
+ _largeImgBeingFetched = largeImgFetch;
+
+ if (_smallImgBeingFetched != null || _largeImgBeingFetched != null)
+ {
+ _fetchThumbnailsToken = new CancellationTokenSource();
+ Task.Run(() => UpdatePresenceIconsAsync(_smallImgBeingFetched, _largeImgBeingFetched, implicitUpdate, _fetchThumbnailsToken.Token));
+ }
+ }
+
public void SetVisibility(bool visible)
{
App.Logger.WriteLine("DiscordRichPresence::SetVisibility", $"Setting presence visibility ({visible})");
@@ -225,13 +369,13 @@ namespace Bloxstrap.Integrations
var universeDetails = activity.UniverseDetails!;
- icon = universeDetails.Thumbnail.ImageUrl;
+ icon = universeDetails.Thumbnail.ImageUrl!;
if (App.Settings.Prop.ShowAccountOnRichPresence)
{
var userDetails = await UserDetails.Fetch(activity.UserId);
- smallImage = userDetails.Thumbnail.ImageUrl;
+ smallImage = userDetails.Thumbnail.ImageUrl!;
smallImageText = $"Playing on {userDetails.Data.DisplayName} (@{userDetails.Data.Name})"; // i.e. "axell (@Axelan_se)"
}
diff --git a/Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs b/Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
new file mode 100644
index 0000000..9d7485f
--- /dev/null
+++ b/Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
@@ -0,0 +1,8 @@
+namespace Bloxstrap.Models.APIs.Roblox
+{
+ internal class ThumbnailBatchResponse
+ {
+ [JsonPropertyName("data")]
+ public ThumbnailResponse[] Data { get; set; } = Array.Empty();
+ }
+}
diff --git a/Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs b/Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
new file mode 100644
index 0000000..b3779e0
--- /dev/null
+++ b/Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
@@ -0,0 +1,34 @@
+namespace Bloxstrap.Models.APIs.Roblox
+{
+ internal class ThumbnailRequest
+ {
+ [JsonPropertyName("requestId")]
+ public string? RequestId { get; set; }
+
+ [JsonPropertyName("targetId")]
+ public ulong TargetId { get; set; }
+
+ ///
+ /// TODO: make this an enum
+ /// List of valid types can be found at https://thumbnails.roblox.com//docs/index.html
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "Avatar";
+
+ ///
+ /// List of valid sizes can be found at https://thumbnails.roblox.com//docs/index.html
+ ///
+ [JsonPropertyName("size")]
+ public string Size { get; set; } = "30x30";
+
+ ///
+ /// TODO: make this an enum
+ /// List of valid types can be found at https://thumbnails.roblox.com//docs/index.html
+ ///
+ [JsonPropertyName("format")]
+ public string Format { get; set; } = "Png";
+
+ [JsonPropertyName("isCircular")]
+ public bool IsCircular { get; set; } = true;
+ }
+}
diff --git a/Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs b/Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs
index 213083c..bc10ba5 100644
--- a/Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs
+++ b/Bloxstrap/Models/APIs/Roblox/ThumbnailResponse.cs
@@ -5,13 +5,31 @@
///
public class ThumbnailResponse
{
+ [JsonPropertyName("requestId")]
+ public string RequestId { get; set; } = null!;
+
+ [JsonPropertyName("errorCode")]
+ public int ErrorCode { get; set; } = 0;
+
+ [JsonPropertyName("errorMessage")]
+ public string? ErrorMessage { get; set; } = null;
+
[JsonPropertyName("targetId")]
public long TargetId { get; set; }
+ ///
+ /// Valid states:
+ /// - Error
+ /// - Completed
+ /// - InReview
+ /// - Pending
+ /// - Blocked
+ /// - TemporarilyUnavailable
+ ///
[JsonPropertyName("state")]
public string State { get; set; } = null!;
[JsonPropertyName("imageUrl")]
- public string ImageUrl { get; set; } = null!;
+ public string? ImageUrl { get; set; } = null!;
}
}
diff --git a/Bloxstrap/Models/ThumbnailCacheEntry.cs b/Bloxstrap/Models/ThumbnailCacheEntry.cs
new file mode 100644
index 0000000..3ce6204
--- /dev/null
+++ b/Bloxstrap/Models/ThumbnailCacheEntry.cs
@@ -0,0 +1,8 @@
+namespace Bloxstrap.Models
+{
+ internal class ThumbnailCacheEntry
+ {
+ public ulong Id { get; set; }
+ public string Url { get; set; } = string.Empty;
+ }
+}
diff --git a/Bloxstrap/Utility/FixedCapacityList.cs b/Bloxstrap/Utility/FixedCapacityList.cs
new file mode 100644
index 0000000..4ea7862
--- /dev/null
+++ b/Bloxstrap/Utility/FixedCapacityList.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Bloxstrap.Utility
+{
+ internal class FixedSizeList : List
+ {
+ public int MaxSize { get; }
+
+ public FixedSizeList(int size)
+ {
+ MaxSize = size;
+ }
+
+ public new void Add(T item)
+ {
+ if (Count >= MaxSize)
+ RemoveAt(Count - 1);
+ base.Add(item);
+ }
+ }
+}
diff --git a/Bloxstrap/Utility/Thumbnails.cs b/Bloxstrap/Utility/Thumbnails.cs
new file mode 100644
index 0000000..00ca77a
--- /dev/null
+++ b/Bloxstrap/Utility/Thumbnails.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Bloxstrap.Utility
+{
+ internal static class Thumbnails
+ {
+ // TODO: remove requests from list once they're finished or failed
+ ///
+ /// Returned array may contain null values
+ ///
+ public static async Task GetThumbnailUrlsAsync(List requests, CancellationToken token)
+ {
+ const string LOG_IDENT = "Thumbnails::GetThumbnailUrlsAsync";
+ const int RETRIES = 5;
+ const int RETRY_TIME_INCREMENT = 500; // ms
+
+ string?[] urls = new string?[requests.Count];
+
+ // assign unique request ids to each request
+ for (int i = 0; i < requests.Count; i++)
+ requests[i].RequestId = i.ToString();
+
+ var payload = new StringContent(JsonSerializer.Serialize(requests));
+
+ ThumbnailResponse[] response = null!;
+
+ for (int i = 1; i <= RETRIES; i++)
+ {
+ var json = await App.HttpClient.PostFromJsonWithRetriesAsync("https://thumbnails.roblox.com/v1/batch", payload, 3, token);
+ if (json == null)
+ throw new InvalidHTTPResponseException("Deserialised ThumbnailBatchResponse is null");
+
+ response = json.Data;
+
+ bool finished = response.All(x => x.State != "Pending");
+ if (finished)
+ break;
+
+ if (i == RETRIES)
+ App.Logger.WriteLine(LOG_IDENT, "Ran out of retries");
+ else
+ await Task.Delay(RETRY_TIME_INCREMENT * i, token);
+ }
+
+ foreach (var item in response)
+ {
+ if (item.State == "Pending")
+ App.Logger.WriteLine(LOG_IDENT, $"{item.TargetId} is still pending");
+ else if (item.State == "Error")
+ App.Logger.WriteLine(LOG_IDENT, $"{item.TargetId} got error code {item.ErrorCode} ({item.ErrorMessage})");
+ else if (item.State != "Completed")
+ App.Logger.WriteLine(LOG_IDENT, $"{item.TargetId} got \"{item.State}\"");
+
+ urls[int.Parse(item.RequestId)] = item.ImageUrl;
+ }
+
+ return urls;
+ }
+
+ public static async Task GetThumbnailUrlAsync(ThumbnailRequest request, CancellationToken token)
+ {
+ const string LOG_IDENT = "Thumbnails::GetThumbnailUrlAsync";
+ const int RETRIES = 5;
+ const int RETRY_TIME_INCREMENT = 500; // ms
+
+ request.RequestId = "0";
+
+ var payload = new StringContent(JsonSerializer.Serialize(new ThumbnailRequest[] { request }));
+
+ ThumbnailResponse response = null!;
+
+ for (int i = 1; i <= RETRIES; i++)
+ {
+ var json = await App.HttpClient.PostFromJsonWithRetriesAsync("https://thumbnails.roblox.com/v1/batch", payload, 3, token);
+ if (json == null)
+ throw new InvalidHTTPResponseException("Deserialised ThumbnailBatchResponse is null");
+
+ response = json.Data[0];
+
+ if (response.State != "Pending")
+ break;
+
+ if (i == RETRIES)
+ App.Logger.WriteLine(LOG_IDENT, "Ran out of retries");
+ else
+ await Task.Delay(RETRY_TIME_INCREMENT * i, token);
+ }
+
+ if (response.State == "Pending")
+ App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} is still pending");
+ else if (response.State == "Error")
+ App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} got error code {response.ErrorCode} ({response.ErrorMessage})");
+ else if (response.State != "Completed")
+ App.Logger.WriteLine(LOG_IDENT, $"{response.TargetId} got \"{response.State}\"");
+
+ return response.ImageUrl;
+ }
+ }
+}