mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-19 00:51:30 -07:00
Merge branch 'main' into feature/update-supported-locales
This commit is contained in:
commit
625a180de7
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
|
26
Bloxstrap/Enums/WebEnvironment.cs
Normal file
26
Bloxstrap/Enums/WebEnvironment.cs
Normal 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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
Bloxstrap/Extensions/HttpClientEx.cs
Normal file
81
Bloxstrap/Extensions/HttpClientEx.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
22
Bloxstrap/Extensions/TEnumEx.cs
Normal file
22
Bloxstrap/Extensions/TEnumEx.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
||||
|
@ -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)"
|
||||
}
|
||||
|
||||
|
@ -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!");
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
8
Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
Normal file
8
Bloxstrap/Models/APIs/Roblox/ThumbnailBatchResponse.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bloxstrap.Models.APIs.Roblox
|
||||
{
|
||||
internal class ThumbnailBatchResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public ThumbnailResponse[] Data { get; set; } = Array.Empty<ThumbnailResponse>();
|
||||
}
|
||||
}
|
34
Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
Normal file
34
Bloxstrap/Models/APIs/Roblox/ThumbnailRequest.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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!;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
8
Bloxstrap/Models/ThumbnailCacheEntry.cs
Normal file
8
Bloxstrap/Models/ThumbnailCacheEntry.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bloxstrap.Models
|
||||
{
|
||||
internal class ThumbnailCacheEntry
|
||||
{
|
||||
public ulong Id { get; set; }
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
68
Bloxstrap/MultiInstanceWatcher.cs
Normal file
68
Bloxstrap/MultiInstanceWatcher.cs
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
80
Bloxstrap/Resources/Strings.Designer.cs
generated
80
Bloxstrap/Resources/Strings.Designer.cs
generated
@ -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 "{0}".
|
||||
/// </summary>
|
||||
@ -1082,7 +1109,7 @@ namespace Bloxstrap.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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'll be able to see where your server is located via [ipinfo.io]({0})..
|
||||
/// </summary>
|
||||
|
@ -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 & 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>
|
@ -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)" />
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
25
Bloxstrap/Utility/FixedCapacityList.cs
Normal file
25
Bloxstrap/Utility/FixedCapacityList.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
103
Bloxstrap/Utility/Thumbnails.cs
Normal file
103
Bloxstrap/Utility/Thumbnails.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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
2
wpfui
@ -1 +1 @@
|
||||
Subproject commit dca423b724ec24bd3377da3a27f4055ae317b50a
|
||||
Subproject commit f710123e72d9dcc8d09fccc4e2a783cc5cf5e652
|
Loading…
Reference in New Issue
Block a user