Compare commits

...

11 Commits

Author SHA1 Message Date
intervinn
91721601bb
Merge b54e44aa58 into bae578f94d 2025-03-15 13:56:31 -04:00
bluepilledgreat
bae578f94d Update wpfui
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-15 17:55:42 +00:00
Matt
3f02c6ba93
Reset ForceReinstall after upgrade (#4890) 2025-03-15 17:55:13 +00:00
Matt
afc3200b68
Improve LaunchSettings constructor (#4889)
* update log ident

* move flagMap inside the ctor function
2025-03-15 17:53:00 +00:00
Matt
d244f42b49
Reintroduce multi-instance launching (#4888) 2025-03-15 17:42:21 +00:00
bluepilledgreat
49fd8eb2d2 improve Watcher ctor macro exclusions
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-15 14:35:09 +00:00
bluepilledgreat
d0f1b9de22 toggle OpenReleaseNotes 2025-03-15 13:04:10 +00:00
Matt
f0eb2eb745
use PathValidator for RenameCustomTheme (#4886) 2025-03-15 10:52:10 +00:00
intervinn
b54e44aa58 remove content 2024-12-30 12:57:24 +03:00
intervinn
3e9e0fba16 Change font family depending on custom font 2024-12-17 17:14:22 +03:00
intervinn
7be311dda6 Split Fonts and CustomFont 2024-12-17 15:05:20 +03:00
16 changed files with 259 additions and 40 deletions

View File

@ -471,21 +471,48 @@ namespace Bloxstrap
}
}
private static void LaunchMultiInstanceWatcher()
{
const string LOG_IDENT = "Bootstrapper::LaunchMultiInstanceWatcher";
if (Utilities.DoesMutexExist("ROBLOX_singletonMutex"))
{
App.Logger.WriteLine(LOG_IDENT, "Roblox singleton mutex already exists");
return;
}
using EventWaitHandle initEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-MultiInstanceWatcherInitialisationFinished");
Process.Start(Paths.Process, "-multiinstancewatcher");
bool initSuccess = initEventHandle.WaitOne(TimeSpan.FromSeconds(2));
if (initSuccess)
App.Logger.WriteLine(LOG_IDENT, "Initialisation finished signalled, continuing.");
else
App.Logger.WriteLine(LOG_IDENT, "Did not receive the initialisation finished signal, continuing.");
}
private void StartRoblox()
{
const string LOG_IDENT = "Bootstrapper::StartRoblox";
SetStatus(Strings.Bootstrapper_Status_Starting);
if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage)
if (_launchMode == LaunchMode.Player)
{
var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
// this needs to be done before roblox launches
if (App.Settings.Prop.MultiInstanceLaunching)
LaunchMultiInstanceWatcher();
if (match.Groups.Count == 2)
_launchCommandLine = _launchCommandLine.Replace(
"robloxLocale:en_us",
$"robloxLocale:{match.Groups[1].Value}",
StringComparison.OrdinalIgnoreCase);
if (App.Settings.Prop.ForceRobloxLanguage)
{
var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant);
if (match.Groups.Count == 2)
_launchCommandLine = _launchCommandLine.Replace(
"robloxLocale:en_us",
$"robloxLocale:{match.Groups[1].Value}",
StringComparison.OrdinalIgnoreCase);
}
}
var startInfo = new ProcessStartInfo()
@ -1109,6 +1136,8 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Registered as {totalSize} KB");
App.State.Prop.ForceReinstall = false;
App.State.Save();
App.RobloxState.Save();

View File

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

View File

@ -59,6 +59,11 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Opening watcher");
LaunchWatcher();
}
else if (App.LaunchSettings.MultiInstanceWatcherFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Opening multi-instance watcher");
LaunchMultiInstanceWatcher();
}
else if (App.LaunchSettings.BackgroundUpdaterFlag.Active)
{
App.Logger.WriteLine(LOG_IDENT, "Opening background updater");
@ -223,7 +228,7 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
}
if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _) && !App.Settings.Prop.MultiInstanceLaunching)
{
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex
// which often hangs around for a few seconds after the window closes
@ -302,6 +307,28 @@ namespace Bloxstrap
});
}
public static void LaunchMultiInstanceWatcher()
{
const string LOG_IDENT = "LaunchHandler::LaunchMultiInstanceWatcher";
App.Logger.WriteLine(LOG_IDENT, "Starting multi-instance watcher");
Task.Run(MultiInstanceWatcher.Run).ContinueWith(t =>
{
App.Logger.WriteLine(LOG_IDENT, "Multi instance watcher task has finished");
if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the multi-instance watcher");
if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception);
}
App.Terminate();
});
}
public static void LaunchBackgroundUpdater()
{
const string LOG_IDENT = "LaunchHandler::LaunchBackgroundUpdater";

View File

@ -12,33 +12,35 @@ namespace Bloxstrap
{
public class LaunchSettings
{
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
public LaunchFlag MenuFlag { get; } = new("preferences,menu,settings");
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag WatcherFlag { get; } = new("watcher");
public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
public LaunchFlag MultiInstanceWatcherFlag { get; } = new("multiinstancewatcher");
public LaunchFlag QuietFlag { get; } = new("quiet");
public LaunchFlag BackgroundUpdaterFlag { get; } = new("backgroundupdater");
public LaunchFlag UninstallFlag { get; } = new("uninstall");
public LaunchFlag QuietFlag { get; } = new("quiet");
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag UninstallFlag { get; } = new("uninstall");
public LaunchFlag NoLaunchFlag { get; } = new("nolaunch");
public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag TestModeFlag { get; } = new("testmode");
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag NoGPUFlag { get; } = new("nogpu");
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag UpgradeFlag { get; } = new("upgrade");
public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag PlayerFlag { get; } = new("player");
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ForceFlag { get; } = new("force");
public LaunchFlag ForceFlag { get; } = new("force");
#if DEBUG
public bool BypassUpdateCheck => true;
@ -55,11 +57,9 @@ namespace Bloxstrap
/// </summary>
public string[] Args { get; private set; }
private readonly Dictionary<string, LaunchFlag> _flagMap = new();
public LaunchSettings(string[] args)
{
const string LOG_IDENT = "LaunchSettings";
const string LOG_IDENT = "LaunchSettings::LaunchSettings";
#if DEBUG
App.Logger.WriteLine(LOG_IDENT, $"Launched with arguments: {string.Join(' ', args)}");
@ -67,6 +67,8 @@ namespace Bloxstrap
Args = args;
Dictionary<string, LaunchFlag> flagMap = new();
// build flag map
foreach (var prop in this.GetType().GetProperties())
{
@ -77,7 +79,7 @@ namespace Bloxstrap
continue;
foreach (string identifier in flag.Identifiers.Split(','))
_flagMap.Add(identifier, flag);
flagMap.Add(identifier, flag);
}
int startIdx = 0;
@ -117,7 +119,7 @@ namespace Bloxstrap
string identifier = arg[1..];
if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
if (!flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
{
App.Logger.WriteLine(LOG_IDENT, $"Unknown argument: {identifier}");
continue;

View File

@ -11,6 +11,7 @@ namespace Bloxstrap.Models.Persistable
public string BootstrapperIconCustomLocation { get; set; } = "";
public Theme Theme { get; set; } = Theme.Default;
public bool CheckForUpdates { get; set; } = true;
public bool MultiInstanceLaunching { get; set; } = false;
public bool ConfirmLaunches { get; set; } = false;
public string Locale { get; set; } = "nil";
public bool ForceRobloxLanguage { get; set; } = false;

View File

@ -0,0 +1,68 @@
namespace Bloxstrap
{
internal static class MultiInstanceWatcher
{
private static int GetOpenProcessesCount()
{
const string LOG_IDENT = "MultiInstanceWatcher::GetOpenProcessesCount";
try
{
// prevent any possible race conditions by checking for bloxstrap processes too
int count = Process.GetProcesses().Count(x => x.ProcessName is "RobloxPlayerBeta" or "Bloxstrap");
count -= 1; // ignore the current process
return count;
}
catch (Exception ex)
{
// everything process related can error at any time
App.Logger.WriteException(LOG_IDENT, ex);
return -1;
}
}
private static void FireInitialisedEvent()
{
using EventWaitHandle initEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "Bloxstrap-MultiInstanceWatcherInitialisationFinished");
initEventHandle.Set();
}
public static void Run()
{
const string LOG_IDENT = "MultiInstanceWatcher::Run";
// try to get the mutex
bool acquiredMutex;
using Mutex mutex = new Mutex(false, "ROBLOX_singletonMutex");
try
{
acquiredMutex = mutex.WaitOne(0);
}
catch (AbandonedMutexException)
{
acquiredMutex = true;
}
if (!acquiredMutex)
{
App.Logger.WriteLine(LOG_IDENT, "Client singleton mutex is already acquired");
FireInitialisedEvent();
return;
}
App.Logger.WriteLine(LOG_IDENT, "Acquired mutex!");
FireInitialisedEvent();
// watch for alive processes
int count;
do
{
Thread.Sleep(5000);
count = GetOpenProcessesCount();
}
while (count == -1 || count > 0); // redo if -1 (one of the Process apis failed)
App.Logger.WriteLine(LOG_IDENT, "All Roblox related processes have closed, exiting!");
}
}
}

View File

@ -26,7 +26,8 @@
public static string Application { get; private set; } = "";
public static string CustomFont => Path.Combine(Modifications, "content\\fonts\\CustomFont.ttf");
public static string Fonts => Path.Combine("content\\fonts");
public static string CustomFont => Path.Combine(Fonts, "CustomFont.ttf");
public static bool Initialized => !String.IsNullOrEmpty(Base);

View File

@ -3425,6 +3425,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Allows for having more than one Roblox game client instance open simultaneously..
/// </summary>
public static string Menu_Integrations_MultiInstanceLaunching_Description {
get {
return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Allow multi-instance launching.
/// </summary>
public static string Menu_Integrations_MultiInstanceLaunching_Title {
get {
return ResourceManager.GetString("Menu.Integrations.MultiInstanceLaunching.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to When in-game, you&apos;ll be able to see where your server is located via [ipinfo.io]({0})..
/// </summary>

View File

@ -1487,4 +1487,10 @@ Defaulting to {1}.</value>
<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>
</root>

View File

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

View File

@ -99,7 +99,7 @@
Description="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Description}">
<StackPanel>
<ui:Button Icon="DocumentAdd16" Content="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Choose}" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding ChooseCustomFontVisibility, Mode=OneWay}" />
<ui:Button Icon="Delete16" Content="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Remove}" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" />
<ui:Button Icon="Delete16" FontFamily="{Binding DeleteCustomFontFontFamily}" Content="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Remove}" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" />
</StackPanel>
</controls:OptionControl>
</StackPanel>

View File

@ -194,11 +194,47 @@ namespace Bloxstrap.UI.ViewModels.Settings
private void RenameCustomTheme()
{
if (SelectedCustomTheme is null)
const string LOG_IDENT = "AppearanceViewModel::RenameCustomTheme";
if (SelectedCustomTheme is null || SelectedCustomTheme == SelectedCustomThemeName)
return;
if (SelectedCustomTheme == SelectedCustomThemeName)
if (string.IsNullOrEmpty(SelectedCustomThemeName))
{
Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameEmpty, MessageBoxImage.Error);
return;
}
var validationResult = PathValidator.IsFileNameValid(SelectedCustomThemeName);
if (validationResult != PathValidator.ValidationResult.Ok)
{
switch (validationResult)
{
case PathValidator.ValidationResult.IllegalCharacter:
Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameIllegalCharacters, MessageBoxImage.Error);
break;
case PathValidator.ValidationResult.ReservedFileName:
Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameReserved, MessageBoxImage.Error);
break;
default:
App.Logger.WriteLine(LOG_IDENT, $"Got unhandled PathValidator::ValidationResult {validationResult}");
Debug.Assert(false);
Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_Unknown, MessageBoxImage.Error);
break;
}
return;
}
// better to check for the file instead of the directory so broken themes can be overwritten
string path = Path.Combine(Paths.CustomThemes, SelectedCustomThemeName, "Theme.xml");
if (File.Exists(path))
{
Frontend.ShowMessageBox(Strings.CustomTheme_Add_Errors_NameTaken, MessageBoxImage.Error);
return;
}
try
{
@ -206,7 +242,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_RenameFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
return;
}

View File

@ -125,6 +125,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
set => App.Settings.Prop.UseDisableAppPatch = value;
}
public bool MultiInstanceLaunchingEnabled
{
get => App.Settings.Prop.MultiInstanceLaunching;
set => App.Settings.Prop.MultiInstanceLaunching = value;
}
public ObservableCollection<CustomIntegration> CustomIntegrations
{
get => App.Settings.Prop.CustomIntegrations;

View File

@ -9,8 +9,10 @@ using Windows.Win32.Foundation;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Models.SettingTasks;
using Bloxstrap.AppData;
using System.Drawing.Text;
using Wpf.Ui.Controls;
using System.Windows.Media;
namespace Bloxstrap.UI.ViewModels.Settings
{
@ -52,9 +54,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
TextFontTask.NewState = dialog.FileName;
}
OnPropertyChanged(nameof(ChooseCustomFontVisibility));
OnPropertyChanged(nameof(DeleteCustomFontVisibility));
OnPropertyChanged(nameof(CustomFontName));
OnPropertyChanged(nameof(DeleteCustomFontFontFamily));
}
public ICommand OpenModsFolderCommand => new RelayCommand(OpenModsFolder);
@ -63,6 +68,18 @@ namespace Bloxstrap.UI.ViewModels.Settings
public Visibility DeleteCustomFontVisibility => !String.IsNullOrEmpty(TextFontTask.NewState) ? Visibility.Visible : Visibility.Collapsed;
public System.Windows.Media.FontFamily DeleteCustomFontFontFamily => new System.Windows.Media.FontFamily($"{TextFontTask.NewState}#{CustomFontName}");
public string CustomFontName
{
get
{
var families = Fonts.GetFontFamilies(TextFontTask.NewState);
var first = families.ElementAt(0);
return first.ToString().Split("#").ElementAt(first.ToString().Split("#").Count() - 1);
}
}
public ICommand ManageCustomFontCommand => new RelayCommand(ManageCustomFont);
public ICommand OpenCompatSettingsCommand => new RelayCommand(OpenCompatSettings);

View File

@ -28,21 +28,21 @@ namespace Bloxstrap
string? watcherDataArg = App.LaunchSettings.WatcherFlag.Data;
#if DEBUG
if (String.IsNullOrEmpty(watcherDataArg))
{
#if DEBUG
string path = new RobloxPlayerData().ExecutablePath;
using var gameClientProcess = Process.Start(path);
_watcherData = new() { ProcessId = gameClientProcess.Id };
}
#else
if (String.IsNullOrEmpty(watcherDataArg))
throw new Exception("Watcher data not specified");
#endif
if (!String.IsNullOrEmpty(watcherDataArg))
}
else
{
_watcherData = JsonSerializer.Deserialize<WatcherData>(Encoding.UTF8.GetString(Convert.FromBase64String(watcherDataArg)));
}
if (_watcherData is null)
throw new Exception("Watcher data is invalid");

2
wpfui

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