Merge branch 'main' into feature/update-supported-locales

This commit is contained in:
bluepilledgreat 2025-03-28 19:34:52 +00:00
commit 625a180de7
35 changed files with 1004 additions and 159 deletions

View File

@ -66,6 +66,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)
{
int exitCodeNum = (int)exitCode;
@ -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<GithubRelease?> 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)

View File

@ -293,12 +293,15 @@ namespace Bloxstrap
await mutex.ReleaseAsync();
if (!App.LaunchSettings.NoLaunchFlag.Active && !_cancelTokenSource.IsCancellationRequested)
{
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();
}
@ -471,13 +474,39 @@ 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)
{
// this needs to be done before roblox launches
if (App.Settings.Prop.MultiInstanceLaunching)
LaunchMultiInstanceWatcher();
if (App.Settings.Prop.ForceRobloxLanguage)
{
var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
@ -487,6 +516,7 @@ namespace Bloxstrap
$"robloxLocale:{match.Groups[1].Value}",
StringComparison.OrdinalIgnoreCase);
}
}
var startInfo = new ProcessStartInfo()
{
@ -829,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);
@ -1109,6 +1145,8 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
App.State.Prop.ForceReinstall = false;
App.State.Save();
App.RobloxState.Save();

View File

@ -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
}
}

View File

@ -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 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 contents.Replace("{0}", moreText);
}
default:
Debug.Assert(false);
return contents;
}
}
}
}

View File

@ -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<HttpResponseMessage> 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<HttpResponseMessage> 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<T?> GetFromJsonWithRetriesAsync<T>(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<T>(stream, cancellationToken: token);
}
public static async Task<T?> PostFromJsonWithRetriesAsync<T>(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<T>(stream, cancellationToken: token);
}
}
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel;
using System.Reflection;
namespace Bloxstrap.Extensions
{
internal static class TEnumEx
{
public static string? GetDescription<TEnum>(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<DescriptionAttribute>();
return attribute?.Description;
}
}
}

View File

@ -9,7 +9,7 @@ namespace Bloxstrap
/// Should this version automatically open the release notes page?
/// Recommended for major updates only.
/// </summary>
private const bool OpenReleaseNotes = false;
private const bool OpenReleaseNotes = true;
private static string DesktopShortcut => Path.Combine(Paths.Desktop, $"{App.ProjectName}.lnk");

View File

@ -13,6 +13,12 @@ namespace Bloxstrap.Integrations
private DiscordRPC.RichPresence? _currentPresence;
private DiscordRPC.RichPresence? _originalPresence;
private FixedSizeList<ThumbnailCacheEntry> _thumbnailCache = new FixedSizeList<ThumbnailCacheEntry>(20);
private ulong? _smallImgBeingFetched = null;
private ulong? _largeImgBeingFetched = null;
private CancellationTokenSource? _fetchThumbnailsToken;
private bool _visible = true;
public DiscordRichPresence(ActivityWatcher activityWatcher)
@ -69,8 +75,107 @@ namespace Bloxstrap.Integrations
}
else if (message.Command == "SetRichPresence")
{
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<ThumbnailRequest>
{
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<Models.BloxstrapRPC.RichPresence>();
@ -117,21 +222,39 @@ namespace Bloxstrap.Integrations
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)
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
{
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;
@ -143,25 +266,46 @@ namespace Bloxstrap.Integrations
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)
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
{
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 (implicitUpdate)
UpdatePresence();
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)
@ -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)"
}

View File

@ -13,6 +13,8 @@ namespace Bloxstrap
/// </summary>
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!");

View File

@ -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";

View File

@ -16,6 +16,8 @@ namespace Bloxstrap
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag MultiInstanceWatcherFlag { get; } = new("multiinstancewatcher");
public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
public LaunchFlag QuietFlag { get; } = new("quiet");
@ -55,11 +57,9 @@ namespace Bloxstrap
/// </summary>
public string[] Args { get; private set; }
private readonly Dictionary<string, LaunchFlag> _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)}");
@ -67,6 +67,8 @@ namespace Bloxstrap
Args = args;
Dictionary<string, LaunchFlag> flagMap = new();
// build flag map
foreach (var prop in this.GetType().GetProperties())
{
@ -77,7 +79,7 @@ namespace Bloxstrap
continue;
foreach (string identifier in flag.Identifiers.Split(','))
_flagMap.Add(identifier, flag);
flagMap.Add(identifier, flag);
}
int startIdx = 0;
@ -117,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;

View File

@ -0,0 +1,8 @@
namespace Bloxstrap.Models.APIs.Roblox
{
internal class ThumbnailBatchResponse
{
[JsonPropertyName("data")]
public ThumbnailResponse[] Data { get; set; } = Array.Empty<ThumbnailResponse>();
}
}

View File

@ -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; }
/// <summary>
/// TODO: make this an enum
/// List of valid types can be found at https://thumbnails.roblox.com//docs/index.html
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = "Avatar";
/// <summary>
/// List of valid sizes can be found at https://thumbnails.roblox.com//docs/index.html
/// </summary>
[JsonPropertyName("size")]
public string Size { get; set; } = "30x30";
/// <summary>
/// TODO: make this an enum
/// List of valid types can be found at https://thumbnails.roblox.com//docs/index.html
/// </summary>
[JsonPropertyName("format")]
public string Format { get; set; } = "Png";
[JsonPropertyName("isCircular")]
public bool IsCircular { get; set; } = true;
}
}

View File

@ -5,13 +5,31 @@
/// </summary>
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; }
/// <summary>
/// Valid states:
/// - Error
/// - Completed
/// - InReview
/// - Pending
/// - Blocked
/// - TemporarilyUnavailable
/// </summary>
[JsonPropertyName("state")]
public string State { get; set; } = null!;
[JsonPropertyName("imageUrl")]
public string ImageUrl { get; set; } = null!;
public string? ImageUrl { get; set; } = null!;
}
}

View File

@ -10,16 +10,20 @@ 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;
public string Locale { get; set; } = "nil";
public bool ForceRobloxLanguage { get; set; } = false;
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;
// integration configuration
public bool EnableActivityTracking { get; set; } = true;

View File

@ -0,0 +1,8 @@
namespace Bloxstrap.Models
{
internal class ThumbnailCacheEntry
{
public ulong Id { get; set; }
public string Url { get; set; } = string.Empty;
}
}

View File

@ -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!");
}
}
}

View File

@ -1,4 +1,4 @@
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="500">
<!-- Put UI elements here -->
<!-- Examples of custom bootstrappers can be found at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
<!-- {0} -->
<!-- {1} -->
</BloxstrapCustomBootstrapper>

View File

@ -1,9 +1,9 @@
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="520" IgnoreTitleBarInset="True" Theme="Default" Margin="30">
<!-- Find more custom bootstrapper examples at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
<!-- {0} -->
<TitleBar Title="" ShowMinimize="False" ShowClose="False" />
<Image Source="{Icon}" Height="100" Width="100" HorizontalAlignment="Center" Margin="0,15,0,0" />
<TextBlock HorizontalAlignment="Center" Name="StatusText" FontSize="20" Margin="0,170,0,0" />
<ProgressBar Width="450" Height="12" Name="PrimaryProgressBar" HorizontalAlignment="Center" Margin="0,200,0,0" />
<Button Content="Cancel" Name="CancelButton" HorizontalAlignment="Center" Margin="0,225,0,0" Height="30" Width="100" />
<Button Content="{Common.Cancel}" Name="CancelButton" HorizontalAlignment="Center" Margin="0,225,0,0" Height="30" Width="100" />
</BloxstrapCustomBootstrapper>

View File

@ -188,7 +188,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Failed to extract all files.
/// Looks up a localized string similar to Failed to extract files.
/// </summary>
public static string Bootstrapper_ExtractionFailed_Title {
get {
@ -235,7 +235,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Failed to apply all modifications.
/// Looks up a localized string similar to Failed to apply modifications.
/// </summary>
public static string Bootstrapper_ModificationsFailed_Title {
get {
@ -973,6 +973,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Custom Theme {0}.
/// </summary>
public static string CustomTheme_DefaultName {
get {
return ResourceManager.GetString("CustomTheme.DefaultName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save changes to {0}?.
/// </summary>
@ -982,6 +991,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Failed to preview theme: {0}.
/// </summary>
public static string CustomTheme_Editor_Errors_PreviewFailed {
get {
return ResourceManager.GetString("CustomTheme.Editor.Errors.PreviewFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Theme Directory.
/// </summary>
@ -1027,6 +1045,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Your theme has been saved!.
/// </summary>
public static string CustomTheme_Editor_Save_Success_Description {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save.Success.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Editing &quot;{0}&quot;.
/// </summary>
@ -1082,7 +1109,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} is missing it&apos;s child.
/// Looks up a localized string similar to {0}.{1} is missing its child.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMissingChild {
get {
@ -1200,7 +1227,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// 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}.
/// </summary>
public static string CustomTheme_Errors_TooManyElements {
get {
@ -1271,6 +1298,33 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Examples of custom bootstrappers can be found at {0}.
/// </summary>
public static string CustomTheme_Templates_Blank_MoreExamples {
get {
return ResourceManager.GetString("CustomTheme.Templates.Blank.MoreExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Put UI elements here.
/// </summary>
public static string CustomTheme_Templates_Blank_UIElements {
get {
return ResourceManager.GetString("CustomTheme.Templates.Blank.UIElements", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Find more custom bootstrapper examples at {0}.
/// </summary>
public static string CustomTheme_Templates_Simple_MoreExamples {
get {
return ResourceManager.GetString("CustomTheme.Templates.Simple.MoreExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add Fast Flag.
/// </summary>
@ -3389,6 +3443,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Allows for having more than one Roblox game client instance open simultaneously..
/// </summary>
public static string Menu_Integrations_MultiInstanceLaunching_Description {
get {
return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Allow multi-instance launching.
/// </summary>
public static string Menu_Integrations_MultiInstanceLaunching_Title {
get {
return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Title", resourceCulture);
}
}
/// <summary>
/// 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>

View File

@ -1274,13 +1274,13 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
<value>Roblox no longer supports Windows 7 or 8.1. To continue playing Roblox, please upgrade to Windows 10 or newer.</value>
</data>
<data name="Bootstrapper.ExtractionFailed.Title" xml:space="preserve">
<value>Failed to extract all files</value>
<value>Failed to extract files</value>
</data>
<data name="Bootstrapper.ExtractionFailed.Message" xml:space="preserve">
<value>Some content may be missing. Force a Roblox reinstallation in settings to fix this.</value>
</data>
<data name="Bootstrapper.ModificationsFailed.Title" xml:space="preserve">
<value>Failed to apply all modifications</value>
<value>Failed to apply modifications</value>
</data>
<data name="Bootstrapper.ModificationsFailed.Message" xml:space="preserve">
<value>Not all modifications will be present in the current launch.</value>
@ -1302,7 +1302,7 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
<value>Custom dialog has already been initialised</value>
</data>
<data name="CustomTheme.Errors.TooManyElements" xml:space="preserve">
<value>Custom bootstrappers can only have a maximum of {0} elements, got {1}.</value>
<value>Custom bootstrappers can only have a maximum of {0} elements, got {1}</value>
<comment>{0} and {1} are numbers</comment>
</data>
<data name="CustomTheme.Errors.VersionNotSet" xml:space="preserve">
@ -1369,7 +1369,7 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMissingChild" xml:space="preserve">
<value>{0}.{1} is missing it's child</value>
<value>{0}.{1} is missing its child</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeParseError" xml:space="preserve">
@ -1474,4 +1474,29 @@ Defaulting to {1}.</value>
<data name="Menu.Behaviour.BackgroundUpdates.Description" xml:space="preserve">
<value>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.</value>
</data>
<data name="CustomTheme.Templates.Blank.UIElements" xml:space="preserve">
<value>Put UI elements here</value>
</data>
<data name="CustomTheme.Templates.Blank.MoreExamples" xml:space="preserve">
<value>Examples of custom bootstrappers can be found at {0}</value>
</data>
<data name="CustomTheme.Templates.Simple.MoreExamples" xml:space="preserve">
<value>Find more custom bootstrapper examples at {0}</value>
</data>
<data name="CustomTheme.DefaultName" xml:space="preserve">
<value>Custom Theme {0}</value>
<comment>{0} is a string (e.g. '1', '1-1234')</comment>
</data>
<data name="Menu.Integrations.MultiInstanceLaunching.Title" xml:space="preserve">
<value>Allow multi-instance launching</value>
</data>
<data name="Menu.Integrations.MultiInstanceLaunching.Description" xml:space="preserve">
<value>Allows for having more than one Roblox game client instance open simultaneously.</value>
</data>
<data name="CustomTheme.Editor.Save.Success.Description" xml:space="preserve">
<value>Your theme has been saved!</value>
</data>
<data name="CustomTheme.Editor.Errors.PreviewFailed" xml:space="preserve">
<value>Failed to preview theme: {0}</value>
</data>
</root>

View File

@ -127,7 +127,7 @@
<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="[Alexa](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)" />

View File

@ -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)

View File

@ -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);
}

View File

@ -29,6 +29,15 @@
<ui:ToggleSwitch IsChecked="{Binding AnalyticsEnabled, Mode=TwoWay}" />
</controls:OptionControl>
<!-- This does not need i18n as this is locked behind "Developer Mode" -->
<controls:OptionControl
Visibility="{Binding Path=WebEnvironmentVisibility, Mode=OneTime}"
Header="Web environment"
Description="Site to use for metrics"
HelpLink="https://admin.bloxstraplabs.com/Wiki/Developers/Web-Environments">
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding WebEnvironments, Mode=OneWay}" SelectedValue="{Binding WebEnvironment, Mode=TwoWay}" />
</controls:OptionControl>
<ui:CardExpander Margin="0,8,0,0" IsExpanded="True">
<ui:CardExpander.Header>
<Grid>

View File

@ -66,6 +66,14 @@
<ui:ToggleSwitch IsChecked="{Binding DiscordAccountOnProfile, Mode=TwoWay}" />
</controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Common_Miscellaneous}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_MultiInstanceLaunching_Title}"
Description="{x:Static resources:Strings.Menu_Integrations_MultiInstanceLaunching_Description}">
<ui:ToggleSwitch IsChecked="{Binding MultiInstanceLaunchingEnabled, Mode=TwoWay}" />
</controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Menu_Integrations_Custom_Title}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_Integrations_Custom_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,8,0,0">

View File

@ -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);
}
}
@ -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)
{

View File

@ -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;
}

View File

@ -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<WebEnvironment>();
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;

View File

@ -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<CustomIntegration> CustomIntegrations
{
get => App.Settings.Prop.CustomIntegrations;

View File

@ -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<T> : List<T>
{
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);
}
}
}

View File

@ -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
/// <remarks>
/// Returned array may contain null values
/// </remarks>
public static async Task<string?[]> GetThumbnailUrlsAsync(List<ThumbnailRequest> 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<ThumbnailBatchResponse>("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<string?> 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<ThumbnailBatchResponse>("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;
}
}
}

View File

@ -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<WatcherData>(Encoding.UTF8.GetString(Convert.FromBase64String(watcherDataArg)));
}
if (_watcherData is null)
throw new Exception("Watcher data is invalid");

View File

@ -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.

2
wpfui

@ -1 +1 @@
Subproject commit dca423b724ec24bd3377da3a27f4055ae317b50a
Subproject commit f710123e72d9dcc8d09fccc4e2a783cc5cf5e652