Compare commits

...

10 Commits

Author SHA1 Message Date
Cubester
690699fcda
Merge d7e5912eb9 into 9ef6579a41 2025-03-14 13:10:43 -06:00
bluepilledgreat
9ef6579a41 update ElementAttributeMissingChild string
Some checks are pending
CI (Debug) / build (push) Waiting to run
CI (Release) / build (push) Waiting to run
CI (Release) / release (push) Blocked by required conditions
CI (Release) / release-test (push) Blocked by required conditions
2025-03-14 14:23:53 +00:00
bluepilledgreat
cdadcb09a3 fix ElementAttributeMultipleDefinitions being wiped
Some checks are pending
CI (Debug) / build (push) Waiting to run
CI (Release) / build (push) Waiting to run
CI (Release) / release (push) Blocked by required conditions
CI (Release) / release-test (push) Blocked by required conditions
2025-03-13 20:17:50 +00:00
bluepilledgreat
cdf129846d update custom theme strings 2025-03-13 20:13:52 +00:00
Matt
ca36306254
Background updates (#4861)
* add background updating

* add RobloxState

* fix potential race condition with RobloxState

* update ForceRobloxReinstallation in menu

* disable AssertReadOnlyDirectory

* add storage space check

* add logging to IsEligibleForBackgroundUpdate

* add a setting to toggle background updates

* fix mutex names being mixed up

* update string

* update strings

* update strings
2025-03-13 16:05:41 +00:00
Cubester
d7e5912eb9
Merge branch 'main' into integrations-upgrade 2025-03-04 19:07:27 -05:00
Cubester
c78b04dce7
Merge branch 'bloxstraplabs:main' into integrations-upgrade 2025-03-02 03:31:32 -05:00
Cubester
9700c61523
Readd "Dispose" 2025-03-02 02:37:24 -05:00
Cubester
c9dabd6b4f
Merge branch 'main' into integrations-upgrade 2024-12-18 22:10:10 -05:00
Cubester
9318813bdb
Custom Integrations Upgrade: Allow launching and closing based on specific games 2024-12-18 21:53:30 -05:00
27 changed files with 648 additions and 111 deletions

View File

@ -39,6 +39,8 @@
<converters:StringFormatConverter x:Key="StringFormatConverter" />
<converters:RangeConverter x:Key="RangeConverter" />
<converters:EnumNameConverter x:Key="EnumNameConverter" />
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -42,7 +42,7 @@ namespace Bloxstrap
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
public static bool IsStudioVisible => !String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid);
public static readonly MD5 MD5Provider = MD5.Create();
@ -54,6 +54,8 @@ namespace Bloxstrap
public static readonly JsonManager<State> State = new();
public static readonly JsonManager<RobloxState> RobloxState = new();
public static readonly FastFlagManager FastFlags = new();
public static readonly HttpClient HttpClient = new(
@ -336,6 +338,7 @@ namespace Bloxstrap
Settings.Load();
State.Load();
RobloxState.Load();
FastFlags.Load();
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))

View File

@ -16,7 +16,7 @@ namespace Bloxstrap.AppData
public override string ExecutableName => "RobloxPlayerBeta.exe";
public override AppState State => App.State.Prop.Player;
public override AppState State => App.RobloxState.Prop.Player;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{

View File

@ -10,7 +10,7 @@
public override string ExecutableName => "RobloxStudioBeta.exe";
public override AppState State => App.State.Prop.Studio;
public override AppState State => App.RobloxState.Prop.Studio;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{

View File

@ -49,6 +49,7 @@ namespace Bloxstrap
private LaunchMode _launchMode;
private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs;
private Version? _latestVersion = null;
private string _latestVersionGuid = null!;
private string _latestVersionDirectory = null!;
private PackageManifest _versionPackageManifest = null!;
@ -60,7 +61,7 @@ namespace Bloxstrap
private long _totalDownloadedBytes = 0;
private bool _packageExtractionSuccess = true;
private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || App.State.Prop.ForceReinstall || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
private bool _noConnection = false;
private AsyncMutex? _mutex;
@ -70,6 +71,9 @@ namespace Bloxstrap
public IBootstrapperDialog? Dialog = null;
public bool IsStudioLaunch => _launchMode != LaunchMode.Player;
public string MutexName { get; set; } = "Bloxstrap-Bootstrapper";
public bool QuitIfMutexExists { get; set; } = false;
#endregion
#region Core
@ -198,22 +202,24 @@ namespace Bloxstrap
// ensure only one instance of the bootstrapper is running at the time
// so that we don't have stuff like two updates happening simultaneously
bool mutexExists = false;
bool mutexExists = Utilities.DoesMutexExist(MutexName);
try
if (mutexExists)
{
Mutex.OpenExisting("Bloxstrap-Bootstrapper").Close();
App.Logger.WriteLine(LOG_IDENT, "Bloxstrap-Bootstrapper mutex exists, waiting...");
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
mutexExists = true;
}
catch (Exception)
{
// no mutex exists
if (!QuitIfMutexExists)
{
App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, waiting...");
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
}
else
{
App.Logger.WriteLine(LOG_IDENT, $"{MutexName} mutex exists, exiting!");
return;
}
}
// wait for mutex to be released if it's not yet
await using var mutex = new AsyncMutex(false, "Bloxstrap-Bootstrapper");
await using var mutex = new AsyncMutex(false, MutexName);
await mutex.AcquireAsync(_cancelTokenSource.Token);
_mutex = mutex;
@ -223,6 +229,7 @@ namespace Bloxstrap
{
App.Settings.Load();
App.State.Load();
App.RobloxState.Load();
}
if (!_noConnection)
@ -237,12 +244,35 @@ namespace Bloxstrap
}
}
CleanupVersionsFolder(); // cleanup after background updater
bool allModificationsApplied = true;
if (!_noConnection)
{
if (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)
await UpgradeRoblox();
{
bool backgroundUpdaterMutexOpen = Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater");
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
backgroundUpdaterMutexOpen = false; // we want to actually update lol
App.Logger.WriteLine(LOG_IDENT, $"Background updater running: {backgroundUpdaterMutexOpen}");
if (backgroundUpdaterMutexOpen && _mustUpgrade)
{
// I am Forced Upgrade, killer of Background Updates
Utilities.KillBackgroundUpdater();
backgroundUpdaterMutexOpen = false;
}
if (!backgroundUpdaterMutexOpen)
{
if (IsEligibleForBackgroundUpdate())
StartBackgroundUpdater();
else
await UpgradeRoblox();
}
}
if (_cancelTokenSource.IsCancellationRequested)
return;
@ -339,11 +369,13 @@ namespace Bloxstrap
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
_latestVersionGuid = clientVersion.VersionGuid;
_latestVersion = Utilities.ParseVersionSafe(clientVersion.Version);
}
else
{
App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments");
_latestVersionGuid = App.LaunchSettings.VersionFlag.Data;
// we can't determine the version
}
_latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid);
@ -366,6 +398,79 @@ namespace Bloxstrap
}
}
private bool IsEligibleForBackgroundUpdate()
{
const string LOG_IDENT = "Bootstrapper::IsEligibleForBackgroundUpdate";
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Is the background updater process");
return false;
}
if (!App.Settings.Prop.BackgroundUpdatesEnabled)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Background updates disabled");
return false;
}
if (IsStudioLaunch)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Studio launch");
return false;
}
if (_mustUpgrade)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Must upgrade is true");
return false;
}
// at least 3GB of free space
const long minimumFreeSpace = 3_000_000_000;
long space = Filesystem.GetFreeDiskSpace(Paths.Base);
if (space < minimumFreeSpace)
{
App.Logger.WriteLine(LOG_IDENT, $"Not eligible: User has {space} free space, at least {minimumFreeSpace} is required");
return false;
}
if (_latestVersion == default)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Latest version is undefined");
return false;
}
Version? currentVersion = Utilities.GetRobloxVersion(AppData);
if (currentVersion == default)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Current version is undefined");
return false;
}
// always normally upgrade for downgrades
if (currentVersion.Minor > _latestVersion.Minor)
{
App.Logger.WriteLine(LOG_IDENT, "Not eligible: Downgrade");
return false;
}
// only background update if we're:
// - one major update behind
// - the same major update
int diff = _latestVersion.Minor - currentVersion.Minor;
if (diff == 0 || diff == 1)
{
App.Logger.WriteLine(LOG_IDENT, "Eligible");
return true;
}
else
{
App.Logger.WriteLine(LOG_IDENT, $"Not eligible: Major version diff is {diff}");
return false;
}
}
private void StartRoblox()
{
const string LOG_IDENT = "Bootstrapper::StartRoblox";
@ -470,30 +575,32 @@ namespace Bloxstrap
// launch custom integrations now
foreach (var integration in App.Settings.Prop.CustomIntegrations)
{
App.Logger.WriteLine(LOG_IDENT, $"Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})");
if (!integration.SpecifyGame) {
App.Logger.WriteLine(LOG_IDENT, $"Launching custom integration '{integration.Name}' ({integration.Location} {integration.LaunchArgs} - autoclose is {integration.AutoClose})");
int pid = 0;
int pid = 0;
try
{
var process = Process.Start(new ProcessStartInfo
try
{
FileName = integration.Location,
Arguments = integration.LaunchArgs.Replace("\r\n", " "),
WorkingDirectory = Path.GetDirectoryName(integration.Location),
UseShellExecute = true
})!;
var process = Process.Start(new ProcessStartInfo
{
FileName = integration.Location,
Arguments = integration.LaunchArgs.Replace("\r\n", " "),
WorkingDirectory = Path.GetDirectoryName(integration.Location),
UseShellExecute = true
})!;
pid = process.Id;
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!");
App.Logger.WriteLine(LOG_IDENT, ex.Message);
}
pid = process.Id;
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}'!");
App.Logger.WriteLine(LOG_IDENT, ex.Message);
}
if (integration.AutoClose && pid != 0)
autoclosePids.Add(pid);
if (integration.AutoClose && pid != 0)
autoclosePids.Add(pid);
}
}
if (App.Settings.Prop.EnableActivityTracking || App.LaunchSettings.TestModeFlag.Active || autoclosePids.Any())
@ -718,13 +825,20 @@ namespace Bloxstrap
{
const string LOG_IDENT = "Bootstrapper::CleanupVersionsFolder";
if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Background updater tried to cleanup, stopping!");
return;
}
foreach (string dir in Directory.GetDirectories(Paths.Versions))
{
string dirName = Path.GetFileName(dir);
if (dirName != App.State.Prop.Player.VersionGuid && dirName != App.State.Prop.Studio.VersionGuid)
if (dirName != App.RobloxState.Prop.Player.VersionGuid && dirName != App.RobloxState.Prop.Studio.VersionGuid)
{
Filesystem.AssertReadOnlyDirectory(dir);
// TODO: this is too expensive
//Filesystem.AssertReadOnlyDirectory(dir);
// check if it's still being used first
// we dont want to accidentally delete the files of a running roblox instance
@ -801,11 +915,11 @@ namespace Bloxstrap
_isInstalling = true;
// make sure nothing is running before continuing upgrade
if (!IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss
if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && !IsStudioLaunch) // TODO: wait for studio processes to close before updating to prevent data loss
KillRobloxPlayers();
// get a fully clean install
if (Directory.Exists(_latestVersionDirectory))
if (!App.LaunchSettings.BackgroundUpdaterFlag.Active && Directory.Exists(_latestVersionDirectory))
{
try
{
@ -958,8 +1072,8 @@ namespace Bloxstrap
var allPackageHashes = new List<string>();
allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values);
allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values);
allPackageHashes.AddRange(App.RobloxState.Prop.Player.PackageHashes.Values);
allPackageHashes.AddRange(App.RobloxState.Prop.Studio.PackageHashes.Values);
if (!App.Settings.Prop.DebugDisableVersionPackageCleanup)
{
@ -988,7 +1102,7 @@ namespace Bloxstrap
AppData.State.Size = distributionSize;
int totalSize = App.State.Prop.Player.Size + App.State.Prop.Studio.Size;
int totalSize = App.RobloxState.Prop.Player.Size + App.RobloxState.Prop.Studio.Size;
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{
@ -998,10 +1112,26 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
App.State.Save();
App.RobloxState.Save();
_isInstalling = false;
}
private static void StartBackgroundUpdater()
{
const string LOG_IDENT = "Bootstrapper::StartBackgroundUpdater";
if (Utilities.DoesMutexExist("Bloxstrap-BackgroundUpdater"))
{
App.Logger.WriteLine(LOG_IDENT, "Background updater already running");
return;
}
App.Logger.WriteLine(LOG_IDENT, "Starting background updater");
Process.Start(Paths.Process, "-backgroundupdater");
}
private async Task<bool> ApplyModifications()
{
const string LOG_IDENT = "Bootstrapper::ApplyModifications";
@ -1135,7 +1265,7 @@ namespace Bloxstrap
var fileRestoreMap = new Dictionary<string, List<string>>();
foreach (string fileLocation in App.State.Prop.ModManifest)
foreach (string fileLocation in App.RobloxState.Prop.ModManifest)
{
if (modFolderFiles.Contains(fileLocation))
continue;
@ -1180,8 +1310,17 @@ namespace Bloxstrap
}
}
App.State.Prop.ModManifest = modFolderFiles;
App.State.Save();
// make sure we're not overwriting a new update
// if we're the background update process, always overwrite
if (App.LaunchSettings.BackgroundUpdaterFlag.Active || !App.RobloxState.HasFileOnDiskChanged())
{
App.RobloxState.Prop.ModManifest = modFolderFiles;
App.RobloxState.Save();
}
else
{
App.Logger.WriteLine(LOG_IDENT, "RobloxState disk mismatch, not saving ModManifest");
}
App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods");

View File

@ -197,7 +197,7 @@ namespace Bloxstrap
var processes = new List<Process>();
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
if (!String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid))
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
if (App.IsStudioVisible)
@ -587,16 +587,23 @@ namespace Bloxstrap
}
}
if (Utilities.CompareVersions(existingVer, "2.8.3") == VersionComparison.LessThan)
if (Utilities.CompareVersions(existingVer, "2.9.0") == VersionComparison.LessThan)
{
// force reinstallation
App.State.Prop.Player.VersionGuid = "";
App.State.Prop.Studio.VersionGuid = "";
// move from App.State to App.RobloxState
if (App.State.Prop.GetDeprecatedPlayer() != null)
App.RobloxState.Prop.Player = App.State.Prop.GetDeprecatedPlayer()!;
if (App.State.Prop.GetDeprecatedStudio() != null)
App.RobloxState.Prop.Studio = App.State.Prop.GetDeprecatedStudio()!;
if (App.State.Prop.GetDeprecatedModManifest() != null)
App.RobloxState.Prop.ModManifest = App.State.Prop.GetDeprecatedModManifest()!;
}
App.Settings.Save();
App.FastFlags.Save();
App.State.Save();
App.RobloxState.Save();
}
if (currentVer is null)

View File

@ -0,0 +1,100 @@
namespace Bloxstrap.Integrations
{
public class IntegrationWatcher : IDisposable
{
private readonly ActivityWatcher _activityWatcher;
private readonly Dictionary<int, CustomIntegration> _activeIntegrations = new();
public IntegrationWatcher(ActivityWatcher activityWatcher)
{
_activityWatcher = activityWatcher;
_activityWatcher.OnGameJoin += OnGameJoin;
_activityWatcher.OnGameLeave += OnGameLeave;
}
private void OnGameJoin(object? sender, EventArgs e)
{
if (!_activityWatcher.InGame)
return;
long currentGameId = _activityWatcher.Data.PlaceId;
foreach (var integration in App.Settings.Prop.CustomIntegrations)
{
if (!integration.SpecifyGame || integration.GameID != currentGameId.ToString())
continue;
LaunchIntegration(integration);
}
}
private void OnGameLeave(object? sender, EventArgs e)
{
foreach (var pid in _activeIntegrations.Keys.ToList())
{
var integration = _activeIntegrations[pid];
if (integration.AutoCloseOnGame)
{
TerminateProcess(pid);
_activeIntegrations.Remove(pid);
}
}
}
private void LaunchIntegration(CustomIntegration integration)
{
const string LOG_IDENT = "IntegrationWatcher::LaunchIntegration";
try
{
var process = Process.Start(new ProcessStartInfo
{
FileName = integration.Location,
Arguments = integration.LaunchArgs.Replace("\r\n", " "),
WorkingDirectory = Path.GetDirectoryName(integration.Location),
UseShellExecute = true
});
if (process != null)
{
App.Logger.WriteLine(LOG_IDENT, $"Integration '{integration.Name}' launched for game ID '{integration.GameID}' (PID {process.Id}).");
_activeIntegrations[process.Id] = integration;
}
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to launch integration '{integration.Name}': {ex.Message}");
}
}
private void TerminateProcess(int pid)
{
const string LOG_IDENT = "IntegrationWatcher::TerminateProcess";
try
{
var process = Process.GetProcessById(pid);
process.Kill();
App.Logger.WriteLine(LOG_IDENT, $"Terminated integration process (PID {pid}).");
}
catch (Exception)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to terminate process (PID {pid}), likely already exited.");
}
}
public void Dispose()
{
foreach (var pid in _activeIntegrations.Keys)
{
TerminateProcess(pid);
}
_activeIntegrations.Clear();
_activityWatcher.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@ -8,6 +8,11 @@ namespace Bloxstrap
public T Prop { get; set; } = new();
/// <summary>
/// The file hash when last retrieved from disk
/// </summary>
public string? LastFileHash { get; private set; }
public virtual string ClassName => typeof(T).Name;
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
@ -22,12 +27,15 @@ namespace Bloxstrap
try
{
T? settings = JsonSerializer.Deserialize<T>(File.ReadAllText(FileLocation));
string contents = File.ReadAllText(FileLocation);
T? settings = JsonSerializer.Deserialize<T>(contents);
if (settings is null)
throw new ArgumentNullException("Deserialization returned null");
Prop = settings;
LastFileHash = MD5Hash.FromString(contents);
App.Logger.WriteLine(LOG_IDENT, "Loaded successfully!");
}
@ -74,7 +82,11 @@ namespace Bloxstrap
try
{
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
string contents = JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(FileLocation, contents);
LastFileHash = MD5Hash.FromString(contents);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
@ -89,5 +101,13 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
}
/// <summary>
/// Is the file on disk different to the one deserialised during this session?
/// </summary>
public bool HasFileOnDiskChanged()
{
return LastFileHash != MD5Hash.FromFile(FileLocation);
}
}
}

View File

@ -4,6 +4,7 @@ using Windows.Win32;
using Windows.Win32.Foundation;
using Bloxstrap.UI.Elements.Dialogs;
using Bloxstrap.Enums;
namespace Bloxstrap
{
@ -58,6 +59,11 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
LaunchWatcher();
}
else if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Opening background updater");
LaunchBackgroundUpdater();
}
else if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
{
App.Logger.WriteLine(LOG_IDENT, $"Opening bootstrapper ({App.LaunchSettings.RobloxLaunchMode})");
@ -295,5 +301,51 @@ namespace Bloxstrap
App.Terminate();
});
}
public static void LaunchBackgroundUpdater()
{
const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater";
// Activate some LaunchFlags we need
App.LaunchSettings.QuietFlag.Active = true;
App.LaunchSettings.NoLaunchFlag.Active = true;
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
App.Bootstrapper = new Bootstrapper(LaunchMode.Player)
{
MutexName = "Bloxstrap-BackgroundUpdater",
QuitIfMutexExists = true
};
CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(() =>
{
App.Logger.WriteLine(LOG_IDENT, "Started event waiter");
using (EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent"))
handle.WaitOne();
App.Logger.WriteLine(LOG_IDENT, "Received close event, killing it all!");
App.Bootstrapper.Cancel();
}, cts.Token);
Task.Run(App.Bootstrapper.Run).ContinueWith(t =>
{
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
cts.Cancel(); // stop event waiter
if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception);
}
App.Terminate();
});
App.Logger.WriteLine(LOG_IDENT, "Exiting");
}
}
}

View File

@ -12,31 +12,33 @@ namespace Bloxstrap
{
public class LaunchSettings
{
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag QuietFlag { get; } = new("quiet");
public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
public LaunchFlag UninstallFlag { get; } = new("uninstall");
public LaunchFlag QuietFlag { get; } = new("quiet");
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag UninstallFlag { get; } = new("uninstall");
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ForceFlag { get; } = new("force");
public LaunchFlag ForceFlag { get; } = new("force");
#if DEBUG
public bool BypassUpdateCheck => true;

View File

@ -5,6 +5,9 @@
public string Name { get; set; } = "";
public string Location { get; set; } = "";
public string LaunchArgs { get; set; } = "";
public bool SpecifyGame { get; set; } = false;
public string GameID { get; set; } = "";
public bool AutoCloseOnGame { get; set; } = true;
public bool AutoClose { get; set; } = true;
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.Persistable
{
public class RobloxState
{
public AppState Player { get; set; } = new();
public AppState Studio { get; set; } = new();
public List<string> ModManifest { get; set; } = new();
}
}

View File

@ -17,6 +17,7 @@ namespace Bloxstrap.Models.Persistable
public bool UseFastFlagManager { get; set; } = true;
public bool WPFSoftwareRender { get; set; } = false;
public bool EnableAnalytics { get; set; } = true;
public bool BackgroundUpdatesEnabled { get; set; } = true;
public bool DebugDisableVersionPackageCleanup { get; set; } = false;
public string? SelectedCustomTheme { get; set; } = null;

View File

@ -6,12 +6,28 @@
public bool PromptWebView2Install { get; set; } = true;
public AppState Player { get; set; } = new();
public AppState Studio { get; set; } = new();
public bool ForceReinstall { get; set; } = false;
public WindowState SettingsWindow { get; set; } = new();
public List<string> ModManifest { get; set; } = new();
#region Deprecated properties
/// <summary>
/// Deprecated, use App.RobloxState.Player
/// </summary>
public AppState? Player { private get; set; }
public AppState? GetDeprecatedPlayer() => Player;
/// <summary>
/// Deprecated, use App.RobloxState.Studio
/// </summary>
public AppState? Studio { private get; set; }
public AppState? GetDeprecatedStudio() => Studio;
/// <summary>
/// Deprecated, use App.RobloxState.ModManifest
/// </summary>
public List<string>? ModManifest { private get; set; }
public List<string>? GetDeprecatedModManifest() => ModManifest;
#endregion
}
}

View File

@ -1064,7 +1064,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to {0} {1} is not a valid {2}.
/// Looks up a localized string similar to {0}.{1} is not a valid {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeInvalidType {
get {
@ -1082,7 +1082,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 {
@ -1109,7 +1109,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to {0} {1} must be larger than {2}.
/// Looks up a localized string similar to {0}.{1} must be larger than {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMustBeLargerThanMin {
get {
@ -1118,7 +1118,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to {0} {1} must be smaller than {2}.
/// Looks up a localized string similar to {0}.{1} must be smaller than {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMustBeSmallerThanMax {
get {
@ -1191,7 +1191,7 @@ namespace Bloxstrap.Resources {
/// <summary>
/// Looks up a localized string similar to Failed to setup custom bootstrapper: {0}.
///Defaulting to Fluent..
///Defaulting to {1}..
/// </summary>
public static string CustomTheme_Errors_SetupFailed {
get {
@ -1218,7 +1218,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to {0} Unknown {1} {2}.
/// Looks up a localized string similar to {0} Unknown {1} &apos;{2}&apos;.
/// </summary>
public static string CustomTheme_Errors_UnknownEnumValue {
get {
@ -2545,6 +2545,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to 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..
/// </summary>
public static string Menu_Behaviour_BackgroundUpdates_Description {
get {
return ResourceManager.GetString("Menu.Behaviour.BackgroundUpdates.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Background updates.
/// </summary>
public static string Menu_Behaviour_BackgroundUpdates_Title {
get {
return ResourceManager.GetString("Menu.Behaviour.BackgroundUpdates.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Prevent against closures of your existing game from accidentally launching another one..
/// </summary>
@ -3262,7 +3280,34 @@ namespace Bloxstrap.Resources {
return ResourceManager.GetString("Menu.Integrations.Custom.AppLocation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Run on specific games.
/// </summary>
public static string Menu_Integrations_Custom_SpecifyGame {
get {
return ResourceManager.GetString("Menu.Integrations.Custom.SpecifyGame", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game ID.
/// </summary>
public static string Menu_Integrations_Custom_GameID {
get {
return ResourceManager.GetString("Menu.Integrations.Custom.GameID", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Auto close when game closes.
/// </summary>
public static string Menu_Integrations_Custom_AutoCloseOnGame {
get {
return ResourceManager.GetString("Menu.Integrations.Custom.AutoCloseOnGame", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Auto close when Roblox closes.
/// </summary>

View File

@ -689,6 +689,15 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.Integrations.Custom.AppLocation" xml:space="preserve">
<value>Application Location</value>
</data>
<data name="Menu.Integrations.Custom.SpecifyGame" xml:space="preserve">
<value>Run on a specific game</value>
</data>
<data name="Menu.Integrations.Custom.GameID" xml:space="preserve">
<value>Game ID</value>
</data>
<data name="Menu.Integrations.Custom.AutoCloseOnGame" xml:space="preserve">
<value>Auto close when the game closes</value>
</data>
<data name="Menu.Integrations.Custom.AutoClose" xml:space="preserve">
<value>Auto close when Roblox closes</value>
</data>
@ -1296,75 +1305,97 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
</data>
<data name="CustomTheme.Errors.InvalidRoot" xml:space="preserve">
<value>Theme XML root is not {0}</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.DialogAlreadyInitialised" xml:space="preserve">
<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>
<comment>{0} and {1} are numbers</comment>
</data>
<data name="CustomTheme.Errors.VersionNotSet" xml:space="preserve">
<value>{0} version is not set</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.VersionNotNumber" xml:space="preserve">
<value>{0} version is not a number</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.VersionNotSupported" xml:space="preserve">
<value>{0} version {1} is no longer supported</value>
<comment>{0} is the element name (e.g. Button), {1} is the version number</comment>
</data>
<data name="CustomTheme.Errors.VersionNotRecognised" xml:space="preserve">
<value>{0} version {1} is not recognised</value>
<comment>{0} is the element name (e.g. Button), {1} is the version number</comment>
</data>
<data name="CustomTheme.Errors.ElementInvalidChild" xml:space="preserve">
<value>{0} cannot have a child of {1}</value>
<comment>{0} and {1} are element names (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.UnknownElement" xml:space="preserve">
<value>Unknown element {0}</value>
<comment>{0} is the element name (e.g. Button)</comment>
</data>
<data name="CustomTheme.Errors.XMLParseFailed" xml:space="preserve">
<value>Failed to parse the theme file: {0}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeConversionError" xml:space="preserve">
<value>{0} has invalid {1}: {2}</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text), {2} is the error reason</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMissing" xml:space="preserve">
<value>Element {0} is missing the {1} attribute</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeInvalidType" xml:space="preserve">
<value>{0} {1} is not a valid {2}</value>
<value>{0}.{1} is not a valid {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the type name (e.g. string)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMustBeLargerThanMin" xml:space="preserve">
<value>{0} {1} must be larger than {2}</value>
<value>{0}.{1} must be larger than {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is a number</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax" xml:space="preserve">
<value>{0} {1} must be smaller than {2}</value>
<value>{0}.{1} must be smaller than {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is a number</comment>
</data>
<data name="CustomTheme.Errors.UnknownEnumValue" xml:space="preserve">
<value>{0} Unknown {1} {2}</value>
<value>{0} Unknown {1} '{2}'</value>
<comment>{0} is the element name (e.g. Button), {1} is the enum name (e.g. WindowCornerType), {2} is the value</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMultipleDefinitions" xml:space="preserve">
<value>{0} can only have one {1} defined</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeMultipleChildren" xml:space="preserve">
<value>{0}.{1} can only have one child</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text)</comment>
</data>
<data name="CustomTheme.Errors.ElementMultipleChildren" xml:space="preserve">
<value>{0} can only have one child</value>
<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">
<value>{0}.{1} could not be parsed into a {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the type name (e.g. string)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeParseErrorNull" xml:space="preserve">
<value>{0}.{1} {2} is null</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the type name (e.g. string)</comment>
</data>
<data name="CustomTheme.Errors.ElementAttributeBlacklistedUriScheme" xml:space="preserve">
<value>{0}.{1} uses blacklisted scheme {2}</value>
<comment>{0}.{1} is the element &amp; attribute name (e.g. Button.Text), {2} is the URI scheme (e.g. http)</comment>
</data>
<data name="CustomTheme.Errors.ElementTypeCreationFailed" xml:space="preserve">
<value>{0} failed to create {1}: {2}</value>
<comment>{0} is the element name (e.g. Button), {1} is the attribute name (e.g. Text), {2} is the error reason</comment>
</data>
<data name="CustomTheme.Editor.Title" xml:space="preserve">
<value>Editing "{0}"</value>
@ -1428,7 +1459,8 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
</data>
<data name="CustomTheme.Errors.SetupFailed" xml:space="preserve">
<value>Failed to setup custom bootstrapper: {0}.
Defaulting to Fluent.</value>
Defaulting to {1}.</value>
<comment>{0} is the error reason, {1} is the theme name (e.g. Bloxstrap (Classic))</comment>
</data>
<data name="Menu.Appearance.CustomThemes.NoneSelected" xml:space="preserve">
<value>No custom theme selected.</value>
@ -1445,4 +1477,10 @@ Defaulting to Fluent.</value>
<data name="Menu.Appearance.CustomThemes.RenameFailed" xml:space="preserve">
<value>Failed to rename custom theme {0}: {1}</value>
</data>
<data name="Menu.Behaviour.BackgroundUpdates.Title" xml:space="preserve">
<value>Background updates</value>
</data>
<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>
</root>

View File

@ -0,0 +1,21 @@
using System.Windows;
using System.Windows.Data;
namespace Bloxstrap.UI.Converters
{
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
return boolValue ? Visibility.Visible : Visibility.Collapsed;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,21 @@
using System.Windows;
using System.Windows.Data;
namespace Bloxstrap.UI.Converters
{
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
return boolValue ? Visibility.Collapsed : Visibility.Visible;
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -110,6 +110,7 @@
<controls:MarkdownTextBlock MarkdownText="[Redusofficial](https://github.com/Redusofficial)" />
<controls:MarkdownTextBlock MarkdownText="[srthMD](https://github.com/srthMD)" />
<controls:MarkdownTextBlock MarkdownText="[axellse](https://github.com/axellse)" />
<controls:MarkdownTextBlock MarkdownText="[CubesterYT](https://github.com/CubesterYT)" />
</StackPanel>
</controls:Expander>

View File

@ -104,7 +104,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
public ByfronDialog()
{
string version = Utilities.GetRobloxVersion(Bootstrapper?.IsStudioLaunch ?? false);
string version = Utilities.GetRobloxVersionStr(Bootstrapper?.IsStudioLaunch ?? false);
_viewModel = new ByfronDialogViewModel(this, version);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;

View File

@ -30,13 +30,19 @@
<ui:ToggleSwitch IsChecked="{Binding ForceRobloxLanguage, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Behaviour_BackgroundUpdates_Title}"
Description="{x:Static resources:Strings.Menu_Behaviour_BackgroundUpdates_Description}">
<ui:ToggleSwitch IsChecked="{Binding BackgroundUpdates, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Title}"
Description="{x:Static resources:Strings.Menu_Behaviour_ForceRobloxReinstall_Description}">
<controls:OptionControl.Style>
<Style TargetType="controls:OptionControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ForceRobloxReinstallation, Mode=OneTime}" Value="True">
<DataTrigger Binding="{Binding IsRobloxInstallationMissing, Mode=OneTime}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>

View File

@ -68,7 +68,7 @@
<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">
<Grid Margin="0,8,0,0" MinHeight="325">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
@ -109,7 +109,11 @@
</Grid>
<TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" PlaceholderText="{Binding Source='/k echo {0}', Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs_Placeholder}}" Text="{Binding SelectedCustomIntegration.LaunchArgs}" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" />
<CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_AutoClose}" IsChecked="{Binding SelectedCustomIntegration.AutoClose}" />
<CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_SpecifyGame}" IsChecked="{Binding SelectedCustomIntegration.SpecifyGame, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_GameID}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" Visibility="{Binding SelectedCustomIntegration.SpecifyGame, Converter={StaticResource BooleanToVisibilityConverter}}" />
<ui:TextBox Margin="0,4,0,0" PlaceholderText="1818" Text="{Binding SelectedCustomIntegration.GameID}" Visibility="{Binding SelectedCustomIntegration.SpecifyGame, Converter={StaticResource BooleanToVisibilityConverter}}" />
<CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_AutoCloseOnGame}" IsChecked="{Binding SelectedCustomIntegration.AutoCloseOnGame, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding SelectedCustomIntegration.SpecifyGame, Converter={StaticResource BooleanToVisibilityConverter}}" />
<CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_AutoClose}" IsChecked="{Binding SelectedCustomIntegration.AutoClose, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding SelectedCustomIntegration.SpecifyGame, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
</StackPanel>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="{x:Static resources:Strings.Menu_Integrations_Custom_NoneSelected}" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.Style>

View File

@ -78,7 +78,7 @@ namespace Bloxstrap.UI
App.Logger.WriteException(LOG_IDENT, ex);
if (!App.LaunchSettings.QuietFlag.Active)
ShowMessageBox(string.Format(Strings.CustomTheme_Errors_SetupFailed, ex.Message), MessageBoxImage.Error);
ShowMessageBox(string.Format(Strings.CustomTheme_Errors_SetupFailed, ex.Message, "Bloxstrap"), MessageBoxImage.Error); // NOTE: Bloxstrap is the theme name
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
}

View File

@ -2,9 +2,6 @@
{
public class BehaviourViewModel : NotifyPropertyChangedViewModel
{
private string _oldPlayerVersionGuid = "";
private string _oldStudioVersionGuid = "";
public bool ConfirmLaunches
{
get => App.Settings.Prop.ConfirmLaunches;
@ -17,26 +14,18 @@
set => App.Settings.Prop.ForceRobloxLanguage = value;
}
public bool BackgroundUpdates
{
get => App.Settings.Prop.BackgroundUpdatesEnabled;
set => App.Settings.Prop.BackgroundUpdatesEnabled = value;
}
public bool IsRobloxInstallationMissing => String.IsNullOrEmpty(App.RobloxState.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.RobloxState.Prop.Studio.VersionGuid);
public bool ForceRobloxReinstallation
{
// wouldnt it be better to check old version guids?
// what about fresh installs?
get => String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid) && String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid);
set
{
if (value)
{
_oldPlayerVersionGuid = App.State.Prop.Player.VersionGuid;
_oldStudioVersionGuid = App.State.Prop.Studio.VersionGuid;
App.State.Prop.Player.VersionGuid = "";
App.State.Prop.Studio.VersionGuid = "";
}
else
{
App.State.Prop.Player.VersionGuid = _oldPlayerVersionGuid;
App.State.Prop.Studio.VersionGuid = _oldStudioVersionGuid;
}
}
get => App.State.Prop.ForceReinstall || IsRobloxInstallationMissing;
set => App.State.Prop.ForceReinstall = value;
}
}
}

View File

@ -75,10 +75,24 @@ namespace Bloxstrap
}
}
public static string GetRobloxVersion(bool studio)
/// <summary>
/// Parses the input version string and prints if fails
/// </summary>
public static Version? ParseVersionSafe(string versionStr)
{
IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData();
const string LOG_IDENT = "Utilities::ParseVersionSafe";
if (!Version.TryParse(versionStr, out Version? version))
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to convert {versionStr} to a valid Version type.");
return version;
}
return version;
}
public static string GetRobloxVersionStr(IAppData data)
{
string playerLocation = data.ExecutablePath;
if (!File.Exists(playerLocation))
@ -92,6 +106,19 @@ namespace Bloxstrap
return versionInfo.ProductVersion.Replace(", ", ".");
}
public static string GetRobloxVersionStr(bool studio)
{
IAppData data = studio ? new RobloxStudioData() : new RobloxPlayerData();
return GetRobloxVersionStr(data);
}
public static Version? GetRobloxVersion(IAppData data)
{
string str = GetRobloxVersionStr(data);
return ParseVersionSafe(str);
}
public static Process[] GetProcessesSafe()
{
const string LOG_IDENT = "Utilities::GetProcessesSafe";
@ -107,5 +134,24 @@ namespace Bloxstrap
return Array.Empty<Process>(); // can we retry?
}
}
public static bool DoesMutexExist(string name)
{
try
{
Mutex.OpenExisting(name).Close();
return true;
}
catch
{
return false;
}
}
public static void KillBackgroundUpdater()
{
using EventWaitHandle handle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-BackgroundUpdaterKillEvent");
handle.Set();
}
}
}

View File

@ -25,6 +25,11 @@ namespace Bloxstrap.Utility
return FromStream(stream);
}
public static string FromString(string str)
{
return FromBytes(Encoding.UTF8.GetBytes(str));
}
public static string Stringify(byte[] hash)
{
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();

View File

@ -9,13 +9,15 @@ namespace Bloxstrap
private readonly InterProcessLock _lock = new("Watcher");
private readonly WatcherData? _watcherData;
private readonly NotifyIconWrapper? _notifyIcon;
public readonly ActivityWatcher? ActivityWatcher;
public readonly DiscordRichPresence? RichPresence;
public readonly IntegrationWatcher? IntegrationWatcher;
public Watcher()
{
const string LOG_IDENT = "Watcher";
@ -63,6 +65,8 @@ namespace Bloxstrap
if (App.Settings.Prop.UseDiscordRichPresence)
RichPresence = new(ActivityWatcher);
IntegrationWatcher = new IntegrationWatcher(ActivityWatcher);
}
_notifyIcon = new(this);
@ -122,6 +126,7 @@ namespace Bloxstrap
{
App.Logger.WriteLine("Watcher::Dispose", "Disposing Watcher");
IntegrationWatcher?.Dispose();
_notifyIcon?.Dispose();
RichPresence?.Dispose();