Compare commits

...

7 Commits

Author SHA1 Message Date
Cubester
eca47f686a
Merge d7e5912eb9 into 893aecbdd1 2025-03-12 17:50:31 -04:00
Matt
893aecbdd1
More arguments + debug settings (#4814)
Some checks failed
CI (Debug) / build (push) Has been cancelled
CI (Release) / build (push) Has been cancelled
CI (Release) / release (push) Has been cancelled
CI (Release) / release-test (push) Has been cancelled
* ability to disable package cleanup

* add version and channel arguments

* ignore duplicate flags

* fix version argument not working

* add the force flag

* fix compiler warnings

* fix indentation
2025-03-12 09:12:31 +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
14 changed files with 308 additions and 52 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

@ -45,8 +45,8 @@ namespace Bloxstrap
private readonly FastZipEvents _fastZipEvents = new();
private readonly CancellationTokenSource _cancelTokenSource = new();
private readonly IAppData AppData;
private readonly LaunchMode _launchMode;
private IAppData AppData = default!;
private LaunchMode _launchMode;
private string _launchCommandLine = App.LaunchSettings.RobloxLaunchArgs;
private string _latestVersionGuid = null!;
@ -60,7 +60,7 @@ namespace Bloxstrap
private long _totalDownloadedBytes = 0;
private bool _packageExtractionSuccess = true;
private bool _mustUpgrade => String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
private bool _mustUpgrade => App.LaunchSettings.ForceFlag.Active || String.IsNullOrEmpty(AppData.State.VersionGuid) || !File.Exists(AppData.ExecutablePath);
private bool _noConnection = false;
private AsyncMutex? _mutex;
@ -91,6 +91,11 @@ namespace Bloxstrap
_fastZipEvents.DirectoryFailure += (_, e) => throw e.Exception;
_fastZipEvents.ProcessFile += (_, e) => e.ContinueRunning = !_cancelTokenSource.IsCancellationRequested;
SetupAppData();
}
private void SetupAppData()
{
AppData = IsStudioLaunch ? new RobloxStudioData() : new RobloxPlayerData();
Deployment.BinaryType = AppData.BinaryType;
}
@ -288,12 +293,17 @@ namespace Bloxstrap
using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel");
var match = Regex.Match(
App.LaunchSettings.RobloxLaunchArgs,
"channel:([a-zA-Z0-9-_]+)",
App.LaunchSettings.RobloxLaunchArgs,
"channel:([a-zA-Z0-9-_]+)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
);
if (match.Groups.Count == 2)
if (App.LaunchSettings.ChannelFlag.Active && !string.IsNullOrEmpty(App.LaunchSettings.ChannelFlag.Data))
{
App.Logger.WriteLine(LOG_IDENT, $"Channel set to {App.LaunchSettings.ChannelFlag.Data} from arguments");
Deployment.Channel = App.LaunchSettings.ChannelFlag.Data.ToLowerInvariant();
}
else if (match.Groups.Count == 2)
{
Deployment.Channel = match.Groups[1].Value.ToLowerInvariant();
}
@ -310,29 +320,50 @@ namespace Bloxstrap
if (!Deployment.IsDefaultChannel)
App.SendStat("robloxChannel", Deployment.Channel);
ClientVersion clientVersion;
try
if (!App.LaunchSettings.VersionFlag.Active || string.IsNullOrEmpty(App.LaunchSettings.VersionFlag.Data))
{
clientVersion = await Deployment.GetInfo();
ClientVersion clientVersion;
try
{
clientVersion = await Deployment.GetInfo();
}
catch (InvalidChannelException ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}");
Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo();
}
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
_latestVersionGuid = clientVersion.VersionGuid;
}
catch (InvalidChannelException ex)
else
{
App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}");
Deployment.Channel = Deployment.DefaultChannel;
clientVersion = await Deployment.GetInfo();
App.Logger.WriteLine(LOG_IDENT, $"Version set to {App.LaunchSettings.VersionFlag.Data} from arguments");
_latestVersionGuid = App.LaunchSettings.VersionFlag.Data;
}
key.SetValueSafe("www.roblox.com", Deployment.IsDefaultChannel ? "" : Deployment.Channel);
_latestVersionGuid = clientVersion.VersionGuid;
_latestVersionDirectory = Path.Combine(Paths.Versions, _latestVersionGuid);
string pkgManifestUrl = Deployment.GetLocation($"/{_latestVersionGuid}-rbxPkgManifest.txt");
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
_versionPackageManifest = new(pkgManifestData);
// this can happen if version is set through arguments
if (_launchMode == LaunchMode.Unknown)
{
App.Logger.WriteLine(LOG_IDENT, "Identifying launch mode from package manifest");
bool isPlayer = _versionPackageManifest.Exists(x => x.Name == "RobloxApp.zip");
App.Logger.WriteLine(LOG_IDENT, $"isPlayer: {isPlayer}");
_launchMode = isPlayer ? LaunchMode.Player : LaunchMode.Studio;
SetupAppData(); // we need to set it up again
}
}
private void StartRoblox()
@ -439,30 +470,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())
@ -930,20 +963,23 @@ namespace Bloxstrap
allPackageHashes.AddRange(App.State.Prop.Player.PackageHashes.Values);
allPackageHashes.AddRange(App.State.Prop.Studio.PackageHashes.Values);
foreach (string hash in cachedPackageHashes)
if (!App.Settings.Prop.DebugDisableVersionPackageCleanup)
{
if (!allPackageHashes.Contains(hash))
foreach (string hash in cachedPackageHashes)
{
App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}");
try
if (!allPackageHashes.Contains(hash))
{
File.Delete(Path.Combine(Paths.Downloads, hash));
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!");
App.Logger.WriteException(LOG_IDENT, ex);
App.Logger.WriteLine(LOG_IDENT, $"Deleting unused package {hash}");
try
{
File.Delete(Path.Combine(Paths.Downloads, hash));
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to delete {hash}!");
App.Logger.WriteException(LOG_IDENT, ex);
}
}
}
}

View File

@ -3,6 +3,10 @@
public enum LaunchMode
{
None,
/// <summary>
/// Launch mode will be determined inside the bootstrapper. Only works if the VersionFlag is set.
/// </summary>
Unknown,
Player,
Studio,
StudioAuth

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

@ -32,6 +32,12 @@ namespace Bloxstrap
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchFlag VersionFlag { get; } = new("version");
public LaunchFlag ChannelFlag { get; } = new("channel");
public LaunchFlag ForceFlag { get; } = new("force");
#if DEBUG
public bool BypassUpdateCheck => true;
#else
@ -87,6 +93,13 @@ namespace Bloxstrap
RobloxLaunchArgs = arg;
startIdx = 1;
}
else if (arg.StartsWith("version-"))
{
App.Logger.WriteLine(LOG_IDENT, "Got version argument");
VersionFlag.Active = true;
VersionFlag.Data = arg;
startIdx = 1;
}
}
// parse
@ -108,6 +121,12 @@ namespace Bloxstrap
continue;
}
if (flag.Active)
{
App.Logger.WriteLine(LOG_IDENT, $"Tried to set {identifier} flag twice");
continue;
}
flag.Active = true;
if (i < Args.Length - 1 && Args[i+1] is string nextArg && !nextArg.StartsWith('-'))
@ -122,6 +141,9 @@ namespace Bloxstrap
}
}
if (VersionFlag.Active)
RobloxLaunchMode = LaunchMode.Unknown; // determine in bootstrapper
if (PlayerFlag.Active)
ParsePlayer(PlayerFlag.Data);
else if (StudioFlag.Active)

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

@ -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 DebugDisableVersionPackageCleanup { get; set; } = false;
public string? SelectedCustomTheme { get; set; } = null;
// integration configuration

View File

@ -3262,7 +3262,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>

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

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

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