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