diff --git a/Bloxstrap/App.xaml b/Bloxstrap/App.xaml index ae4bfac..6f8c77d 100644 --- a/Bloxstrap/App.xaml +++ b/Bloxstrap/App.xaml @@ -37,6 +37,8 @@ + + diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 7a7cfc7..149108f 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -426,30 +426,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()) diff --git a/Bloxstrap/Integrations/IntegrationWatcher.cs b/Bloxstrap/Integrations/IntegrationWatcher.cs new file mode 100644 index 0000000..5a535cc --- /dev/null +++ b/Bloxstrap/Integrations/IntegrationWatcher.cs @@ -0,0 +1,100 @@ +namespace Bloxstrap.Integrations +{ + public class IntegrationWatcher : IDisposable + { + private readonly ActivityWatcher _activityWatcher; + private readonly Dictionary _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(); + + GC.SuppressFinalize(this); + } + } +} diff --git a/Bloxstrap/Models/CustomIntegration.cs b/Bloxstrap/Models/CustomIntegration.cs index d293f61..0677cf5 100644 --- a/Bloxstrap/Models/CustomIntegration.cs +++ b/Bloxstrap/Models/CustomIntegration.cs @@ -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; } } diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index fd14b8f..265377c 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -2730,7 +2730,34 @@ namespace Bloxstrap.Resources { return ResourceManager.GetString("Menu.Integrations.Custom.AppLocation", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Run on specific games. + /// + public static string Menu_Integrations_Custom_SpecifyGame { + get { + return ResourceManager.GetString("Menu.Integrations.Custom.SpecifyGame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game ID. + /// + public static string Menu_Integrations_Custom_GameID { + get { + return ResourceManager.GetString("Menu.Integrations.Custom.GameID", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Auto close when game closes. + /// + public static string Menu_Integrations_Custom_AutoCloseOnGame { + get { + return ResourceManager.GetString("Menu.Integrations.Custom.AutoCloseOnGame", resourceCulture); + } + } + /// /// Looks up a localized string similar to Auto close when Roblox closes. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index c8a5479..996eec2 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -689,6 +689,15 @@ Selecting 'No' will ignore this warning and continue installation. Application Location + + Run on a specific game + + + Game ID + + + Auto close when the game closes + Auto close when Roblox closes diff --git a/Bloxstrap/UI/Converters/BooleanToVisibilityConverter.cs b/Bloxstrap/UI/Converters/BooleanToVisibilityConverter.cs new file mode 100644 index 0000000..3c94d3b --- /dev/null +++ b/Bloxstrap/UI/Converters/BooleanToVisibilityConverter.cs @@ -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(); + } + } +} diff --git a/Bloxstrap/UI/Converters/InverseBooleanToVisibilityConverter.cs b/Bloxstrap/UI/Converters/InverseBooleanToVisibilityConverter.cs new file mode 100644 index 0000000..09ea37b --- /dev/null +++ b/Bloxstrap/UI/Converters/InverseBooleanToVisibilityConverter.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml b/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml index 486cd12..02c1559 100644 --- a/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml +++ b/Bloxstrap/UI/Elements/About/Pages/AboutPage.xaml @@ -110,6 +110,7 @@ + diff --git a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml index 0e391bb..fa22a58 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml @@ -68,7 +68,7 @@ - + @@ -109,7 +109,11 @@ - + + + + + diff --git a/Bloxstrap/Watcher.cs b/Bloxstrap/Watcher.cs index eef397e..c8bd699 100644 --- a/Bloxstrap/Watcher.cs +++ b/Bloxstrap/Watcher.cs @@ -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();