diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini
index 955bf139..315a4be0 100644
--- a/data/locale/en-US.ini
+++ b/data/locale/en-US.ini
@@ -769,6 +769,8 @@ AdvSceneSwitcher.condition.streamDeck.checkData="Check data field"
AdvSceneSwitcher.condition.streamDeck.startListen="Start listening"
AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
AdvSceneSwitcher.condition.streamDeck.pluginDownload="
The Stream Deck plugin can be found here on GitHub.
"
+AdvSceneSwitcher.condition.gameCapture="Game capture"
+AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
AdvSceneSwitcher.condition.screenshot="Screenshot"
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
@@ -1980,6 +1982,13 @@ AdvSceneSwitcher.tempVar.queue.running.description="Returns \"true\" if the queu
AdvSceneSwitcher.tempVar.screenshot.lastScreenshotPath="Last screenshot path"
AdvSceneSwitcher.tempVar.screenshot.lastScreenshotPath.description="The filesystem path to the last screenshot taken."
+AdvSceneSwitcher.tempVar.gameCapture.title="Title"
+AdvSceneSwitcher.tempVar.gameCapture.title.description="Window title of the application captured by the source."
+AdvSceneSwitcher.tempVar.gameCapture.class="Class"
+AdvSceneSwitcher.tempVar.gameCapture.class.description="Window class of the application captured by the source."
+AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
+AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
+
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"
diff --git a/plugins/base/CMakeLists.txt b/plugins/base/CMakeLists.txt
index 7997700d..a066ba8c 100644
--- a/plugins/base/CMakeLists.txt
+++ b/plugins/base/CMakeLists.txt
@@ -89,6 +89,8 @@ target_sources(
macro-condition-filter.hpp
macro-condition-folder.cpp
macro-condition-folder.hpp
+ macro-condition-game-capture.cpp
+ macro-condition-game-capture.hpp
macro-condition-hotkey.cpp
macro-condition-hotkey.hpp
macro-condition-idle.cpp
diff --git a/plugins/base/macro-condition-game-capture.cpp b/plugins/base/macro-condition-game-capture.cpp
new file mode 100644
index 00000000..f2593e61
--- /dev/null
+++ b/plugins/base/macro-condition-game-capture.cpp
@@ -0,0 +1,197 @@
+#include "macro-condition-game-capture.hpp"
+#include "layout-helpers.hpp"
+
+namespace advss {
+
+const std::string MacroConditionGameCapture::id = "game_capture";
+
+#ifdef _WIN32
+bool MacroConditionGameCapture::_registered = MacroConditionFactory::Register(
+ MacroConditionGameCapture::id,
+ {MacroConditionGameCapture::Create,
+ MacroConditionGameCaptureEdit::Create,
+ "AdvSceneSwitcher.condition.gameCapture"});
+#else
+bool MacroConditionGameCapture::_registered =
+ false; // For now the 'game_capture' source only exists on Windows
+#endif
+
+bool MacroConditionGameCapture::CheckCondition()
+{
+ auto source = OBSGetStrongRef(_source.GetSource());
+ if (source != _lastSource) {
+ SetupSignalHandler(source);
+ return false;
+ }
+
+ std::lock_guard lock(_mtx);
+ if (_hooked) {
+ SetTempVarValue("title", _title);
+ SetTempVarValue("class", _class);
+ SetTempVarValue("executable", _executable);
+ }
+
+ return _hooked;
+}
+
+bool MacroConditionGameCapture::Save(obs_data_t *obj) const
+{
+ MacroCondition::Save(obj);
+ _source.Save(obj);
+ return true;
+}
+
+bool MacroConditionGameCapture::Load(obs_data_t *obj)
+{
+ MacroCondition::Load(obj);
+ _source.Load(obj);
+ SetupSignalHandler(OBSGetStrongRef(_source.GetSource()));
+ return true;
+}
+
+void MacroConditionGameCapture::GetCalldataInfo(calldata_t *cd)
+{
+ const char *title = "";
+ if (!calldata_get_string(cd, "title", &title)) {
+ blog(LOG_WARNING, "%s failed to get title", __func__);
+ }
+ _title = title;
+ const char *className = "";
+ if (!calldata_get_string(cd, "class", &className)) {
+ blog(LOG_WARNING, "%s failed to get class", __func__);
+ }
+ _class = className;
+ const char *executable = "";
+ if (!calldata_get_string(cd, "executable", &executable)) {
+ blog(LOG_WARNING, "%s failed to get executable", __func__);
+ }
+ _executable = executable;
+}
+
+void MacroConditionGameCapture::HookedSignalReceived(void *data, calldata_t *cd)
+{
+ auto condition = static_cast(data);
+ std::lock_guard lock(condition->_mtx);
+ condition->GetCalldataInfo(cd);
+ condition->_hooked = true;
+}
+
+void MacroConditionGameCapture::UnhookedSignalReceived(void *data, calldata_t *)
+{
+ auto condition = static_cast(data);
+ std::lock_guard lock(condition->_mtx);
+ condition->_hooked = false;
+}
+
+void MacroConditionGameCapture::SetupTempVars()
+{
+ MacroCondition::SetupTempVars();
+ AddTempvar(
+ "title",
+ obs_module_text("AdvSceneSwitcher.tempVar.gameCapture.title"),
+ obs_module_text(
+ "AdvSceneSwitcher.tempVar.gameCapture.title.description"));
+ AddTempvar(
+ "class",
+ obs_module_text("AdvSceneSwitcher.tempVar.gameCapture.class"),
+ obs_module_text(
+ "AdvSceneSwitcher.tempVar.gameCapture.class.description"));
+ AddTempvar(
+ "executable",
+ obs_module_text(
+ "AdvSceneSwitcher.tempVar.gameCapture.executable"),
+ obs_module_text(
+ "AdvSceneSwitcher.tempVar.gameCapture.executable.description"));
+}
+
+void MacroConditionGameCapture::SetupSignalHandler(obs_source_t *source)
+{
+ signal_handler_t *sh = obs_source_get_signal_handler(source);
+ _hookSignal = OBSSignal(sh, "hooked", HookedSignalReceived, this);
+ _unhookSignal = OBSSignal(sh, "unhooked", UnhookedSignalReceived, this);
+ _lastSource = source;
+
+ SetupInitialState(source);
+}
+
+void MacroConditionGameCapture::SetupInitialState(obs_source_t *source)
+{
+ std::lock_guard lock(_mtx);
+
+ _hooked = false;
+ if (!source) {
+ return;
+ }
+
+ calldata_t cd;
+ calldata_init(&cd);
+ auto ph = obs_source_get_proc_handler(source);
+ if (!proc_handler_call(ph, "get_hooked", &cd)) {
+ blog(LOG_WARNING,
+ "%s failed to call proc_handler for 'get_hooked'",
+ __func__);
+ return;
+ }
+
+ if (!calldata_get_bool(&cd, "hooked", &_hooked)) {
+ blog(LOG_WARNING, "%s failed to get hooked state", __func__);
+ }
+ GetCalldataInfo(&cd);
+
+ calldata_free(&cd);
+}
+
+static QStringList getGameCaptureSourceNames()
+{
+ auto sourceEnum = [](void *param, obs_source_t *source) -> bool /* -- */
+ {
+ QStringList *list = reinterpret_cast(param);
+
+ if (strcmp(obs_source_get_unversioned_id(source),
+ "game_capture") == 0) {
+ *list << obs_source_get_name(source);
+ }
+ return true;
+ };
+
+ QStringList list;
+ obs_enum_sources(sourceEnum, &list);
+ return list;
+}
+
+MacroConditionGameCaptureEdit::MacroConditionGameCaptureEdit(
+ QWidget *parent, std::shared_ptr entryData)
+ : QWidget(parent),
+ _sources(new SourceSelectionWidget(this, QStringList(), true))
+{
+ auto sources = getGameCaptureSourceNames();
+ sources.sort();
+ _sources->SetSourceNameList(sources);
+
+ QWidget::connect(_sources,
+ SIGNAL(SourceChanged(const SourceSelection &)), this,
+ SLOT(SourceChanged(const SourceSelection &)));
+
+ auto layout = new QHBoxLayout;
+ PlaceWidgets(
+ obs_module_text("AdvSceneSwitcher.condition.gameCapture.entry"),
+ layout, {{"{{sources}}", _sources}});
+ setLayout(layout);
+
+ _entryData = entryData;
+ UpdateEntryData();
+ _loading = false;
+}
+
+void MacroConditionGameCaptureEdit::SourceChanged(const SourceSelection &source)
+{
+ GUARD_LOADING_AND_LOCK();
+ _entryData->_source = source;
+}
+
+void MacroConditionGameCaptureEdit::UpdateEntryData()
+{
+ _sources->SetSource(_entryData->_source);
+}
+
+} // namespace advss
diff --git a/plugins/base/macro-condition-game-capture.hpp b/plugins/base/macro-condition-game-capture.hpp
new file mode 100644
index 00000000..f78076ae
--- /dev/null
+++ b/plugins/base/macro-condition-game-capture.hpp
@@ -0,0 +1,74 @@
+#pragma once
+#include "macro-condition-edit.hpp"
+#include "source-selection.hpp"
+
+#include
+#include
+
+namespace advss {
+
+class MacroConditionGameCapture : public MacroCondition {
+public:
+ MacroConditionGameCapture(Macro *m) : MacroCondition(m) {}
+ bool CheckCondition();
+ bool Save(obs_data_t *obj) const;
+ bool Load(obs_data_t *obj);
+ std::string GetId() const { return id; };
+ static std::shared_ptr Create(Macro *m)
+ {
+ return std::make_shared(m);
+ }
+
+ SourceSelection _source;
+
+private:
+ static void HookedSignalReceived(void *data, calldata_t *);
+ static void UnhookedSignalReceived(void *data, calldata_t *);
+
+ void SetupTempVars();
+ void SetupSignalHandler(obs_source_t *source);
+ void SetupInitialState(obs_source_t *source);
+ void GetCalldataInfo(calldata_t *cd);
+
+ obs_source_t *_lastSource = nullptr;
+ OBSSignal _hookSignal;
+ OBSSignal _unhookSignal;
+
+ bool _hooked = false;
+ std::string _title = "";
+ std::string _class = "";
+ std::string _executable = "";
+ std::mutex _mtx;
+
+ static bool _registered;
+ static const std::string id;
+};
+
+class MacroConditionGameCaptureEdit : public QWidget {
+ Q_OBJECT
+
+public:
+ MacroConditionGameCaptureEdit(
+ QWidget *parent,
+ std::shared_ptr cond = nullptr);
+ void UpdateEntryData();
+ static QWidget *Create(QWidget *parent,
+ std::shared_ptr cond)
+ {
+ return new MacroConditionGameCaptureEdit(
+ parent,
+ std::dynamic_pointer_cast(
+ cond));
+ }
+
+private slots:
+ void SourceChanged(const SourceSelection &);
+
+private:
+ SourceSelectionWidget *_sources;
+ std::shared_ptr _entryData;
+
+ bool _loading = true;
+};
+
+} // namespace advss