SceneSwitcher/lib/macro/macro.cpp
WarmUpTill ba38b8bf27
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Don't block UI while executing long runnig actions
The previous approach had the problem of losing any action internal
state changes in the created copy.

Revert "Fix temp var values of actions not being accessible"
This reverts commit df42538319.
Revert "Don't block UI while running actions"
This reverts commit a01d26e25d.
2026-04-04 21:14:05 +02:00

1346 lines
34 KiB
C++

#include "macro.hpp"
#include "macro-action-factory.hpp"
#include "macro-condition-factory.hpp"
#include "macro-helpers.hpp"
#include "macro-settings.hpp"
#include "plugin-state-helpers.hpp"
#include "splitter-helpers.hpp"
#include "sync-helpers.hpp"
#include <chrono>
#include <limits>
#undef max
#include <obs-frontend-api.h>
#include <QAction>
#include <QMainWindow>
#include <unordered_map>
namespace advss {
Macro::Macro(const std::string &name) : _dockSettings(this)
{
SetName(name);
}
Macro::Macro(const std::string &name, const GlobalMacroSettings &settings)
: Macro(name)
{
if (settings._newMacroRegisterHotkeys) {
SetupHotkeys();
}
_registerHotkeys = settings._newMacroRegisterHotkeys;
_checkInParallel = settings._newMacroCheckInParallel;
_useShortCircuitEvaluation =
settings._newMacroUseShortCircuitEvaluation;
}
Macro::~Macro()
{
_die = true;
Stop();
ClearHotkeys();
}
std::shared_ptr<Macro>
Macro::CreateGroup(const std::string &name,
std::vector<std::shared_ptr<Macro>> &children)
{
auto group = std::make_shared<Macro>(name);
group->_registerHotkeys = false;
for (auto &c : children) {
c->SetParent(group);
}
group->_isGroup = true;
group->_groupSize = children.size();
return group;
}
void Macro::RemoveGroup(std::shared_ptr<Macro> group)
{
auto &macros = GetTopLevelMacros();
auto it = std::find(macros.begin(), macros.end(), group);
if (it == macros.end()) {
return;
}
auto size = group->GroupSize();
for (uint32_t i = 1; i <= size; i++) {
auto m = std::next(it, i);
(*m)->SetParent(nullptr);
}
macros.erase(it);
}
void Macro::PrepareMoveToGroup(Macro *group, std::shared_ptr<Macro> item)
{
auto &macros = GetTopLevelMacros();
for (const auto &m : macros) {
if (m.get() == group) {
PrepareMoveToGroup(m, item);
return;
}
}
PrepareMoveToGroup(std::shared_ptr<Macro>(), item);
}
void Macro::PrepareMoveToGroup(std::shared_ptr<Macro> group,
std::shared_ptr<Macro> item)
{
if (!item) {
return;
}
// Potentially remove from old group
auto oldGroup = item->Parent();
if (oldGroup) {
oldGroup->_groupSize--;
}
item->SetParent(group);
if (group) {
group->_groupSize++;
}
}
static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
{
using namespace std::chrono_literals;
static constexpr auto perfLogThreshold = 300ms;
const auto startTime = std::chrono::high_resolution_clock::now();
bool conditionMatched = false;
condition->WithLock([&condition, &conditionMatched]() {
conditionMatched = condition->EvaluateCondition();
});
const auto endTime = std::chrono::high_resolution_clock::now();
const auto timeSpent = endTime - startTime;
if (timeSpent >= perfLogThreshold) {
const long int ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
timeSpent)
.count();
blog(LOG_WARNING,
"spent %ld ms in %s condition check of macro '%s'!", ms,
condition->GetId().c_str(),
condition->GetMacro()->Name().c_str());
}
return conditionMatched;
}
bool Macro::CheckConditionHelper(
const std::shared_ptr<MacroCondition> &condition) const
{
bool conditionMatched = false;
bool wasEvaluated = false;
const auto evaluateCondition = [&condition, &conditionMatched,
&wasEvaluated]() -> bool {
conditionMatched = checkCondition(condition);
conditionMatched =
condition->CheckDurationModifier(conditionMatched);
wasEvaluated = true;
return conditionMatched;
};
const auto logicType = condition->GetLogicType();
if (logicType == Logic::Type::NONE) {
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
condition->GetId().c_str(), _name.c_str());
if (!_useShortCircuitEvaluation) {
(void)evaluateCondition();
}
return _matched;
}
bool result = _useShortCircuitEvaluation
// Evaluate the condition result if needed
? Logic::ApplyConditionLogic(logicType, _matched,
evaluateCondition,
_name.c_str())
// Evaluate the condition result right away
: Logic::ApplyConditionLogic(logicType, _matched,
evaluateCondition(),
_name.c_str());
const bool isNegativeLogicType = Logic::IsNegationType(logicType);
if (wasEvaluated && ((conditionMatched && !isNegativeLogicType) ||
(!conditionMatched && isNegativeLogicType))) {
condition->EnableHighlight();
}
if (VerboseLoggingEnabled()) {
if (wasEvaluated) {
blog(LOG_INFO, "condition %s returned %d",
condition->GetId().c_str(), conditionMatched);
} else {
blog(LOG_INFO,
"condition %s evaluation skipped (short circuit)",
condition->GetId().c_str());
}
}
return result;
}
bool Macro::CheckConditions(bool ignorePause)
{
if (_isGroup) {
return false;
}
const auto checkConditionsTask =
[this,
ignorePause](const std::deque<std::shared_ptr<MacroCondition>>
&conditions) {
for (auto &condition : conditions) {
if (!condition) {
continue;
}
if (_paused && !ignorePause) {
vblog(LOG_INFO, "Macro %s is paused",
_name.c_str());
return false;
}
_matched = CheckConditionHelper(condition);
}
return _matched;
};
if (CheckInParallel()) {
if (!_conditionCheckFuture.valid()) {
_stop = false;
_matched = false;
_conditionCheckFuture = std::async(
std::launch::async,
[this, checkConditionsTask]() {
// Copy to avoid settings modifications
// causing issues
const auto conditionsCopy = _conditions;
checkConditionsTask(conditionsCopy);
});
return false;
}
if (_conditionCheckFuture.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) {
vblog(LOG_INFO,
"Macro %s still waiting for condition check result",
_name.c_str());
return false;
}
_conditionCheckFuture.get();
} else {
_stop = false;
_matched = false;
_matched = checkConditionsTask(_conditions);
}
vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);
_actionModeMatch = false;
switch (_actionTriggerMode) {
case Macro::ActionTriggerMode::ALWAYS:
_actionModeMatch = true;
break;
case Macro::ActionTriggerMode::MACRO_RESULT_CHANGED:
_actionModeMatch = _lastMatched != _matched;
break;
case Macro::ActionTriggerMode::ANY_CONDITION_CHANGED:
for (const auto &condition : _conditions) {
if (condition->HasChanged()) {
_actionModeMatch = true;
}
}
break;
case Macro::ActionTriggerMode::ANY_CONDITION_TRIGGERED:
for (const auto &condition : _conditions) {
if (condition->IsRisingEdge()) {
_actionModeMatch = true;
}
}
break;
default:
break;
}
const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
: (_elseActions.size() > 0);
if (!_actionModeMatch && hasActionsToExecute) {
_lastActionRunModePreventTime =
std::chrono::high_resolution_clock::now();
}
_lastMatched = _matched;
_lastCheckTime = std::chrono::high_resolution_clock::now();
return _matched;
}
bool Macro::PerformActions(bool match, bool forceParallel, bool ignorePause)
{
if (_actionRunFuture.valid() &&
_actionRunFuture.wait_for(std::chrono::seconds(0)) !=
std::future_status::ready) {
vblog(LOG_INFO, "Macro %s already running", _name.c_str());
if (!_stopActionsIfNotDone) {
return !forceParallel;
}
Stop();
vblog(LOG_INFO, "Stopped macro %s actions to rerun them",
_name.c_str());
}
std::function<bool(bool)> runFunc =
match ? std::bind(&Macro::RunActions, this,
std::placeholders::_1)
: std::bind(&Macro::RunElseActions, this,
std::placeholders::_1);
_stop = false;
bool ret = true;
if (_runInParallel || forceParallel) {
if (_actionRunFuture.valid()) {
_actionRunFuture.get();
}
_actionRunFuture = std::async(std::launch::async,
[this, runFunc, ignorePause] {
runFunc(ignorePause);
});
} else {
ret = runFunc(ignorePause);
}
_lastExecutionTime = std::chrono::high_resolution_clock::now();
auto group = _parent.lock();
if (group) {
group->_lastExecutionTime = _lastExecutionTime;
}
if (_runCount != std::numeric_limits<int>::max()) {
_runCount++;
}
return ret;
}
bool Macro::WasExecutedSince(const TimePoint &time) const
{
return _lastExecutionTime > time;
}
bool Macro::ActionTriggerModePreventedActionsSince(const TimePoint &time) const
{
return _lastActionRunModePreventTime > time;
}
Macro::TimePoint Macro::GetLastExecutionTime() const
{
return _lastExecutionTime;
}
bool Macro::ConditionsShouldBeChecked() const
{
if (!_useCustomConditionCheckInterval) {
return true;
}
const auto timePassed = std::chrono::high_resolution_clock::now() -
LastConditionCheckTime();
const auto timePassedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
timePassed);
return timePassedMs.count() >=
_customConditionCheckInterval.Milliseconds();
}
bool Macro::ShouldRunActions() const
{
if (CheckInParallel() && _conditionCheckFuture.valid()) {
vblog(LOG_INFO,
"%s not ready to perform actions as condition check is still running",
_name.c_str());
return false;
}
const bool hasActionsToExecute =
!_paused && (_matched || _elseActions.size() > 0) &&
_actionModeMatch;
if (VerboseLoggingEnabled() && !_actionModeMatch) {
if (_matched && _actions.size() > 0) {
blog(LOG_INFO, "skip actions for Macro %s (on change)",
_name.c_str());
}
if (!_matched && _elseActions.size() > 0) {
blog(LOG_INFO,
"skip else actions for Macro %s (on change)",
_name.c_str());
}
}
return hasActionsToExecute;
}
void Macro::SetName(const std::string &name)
{
_name = name;
SetHotkeysDesc();
_dockSettings.HandleMacroNameChange();
}
void Macro::ResetTimers()
{
for (auto &c : _conditions) {
c->ResetDuration();
}
_lastCheckTime = {};
_lastExecutionTime = {};
}
void Macro::SetActionTriggerMode(ActionTriggerMode mode)
{
_actionTriggerMode = mode;
}
Macro::ActionTriggerMode Macro::GetActionTriggerMode() const
{
return _actionTriggerMode;
}
bool Macro::RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actionsToRun,
bool ignorePause)
{
if (_paused && !ignorePause) {
return true;
}
// Create copy of action list as elements might be removed, inserted, or
// reordered while actions are currently being executed.
auto actions = actionsToRun;
bool actionsExecutedSuccessfully = true;
for (auto &action : actions) {
if (!action) {
continue;
}
if (action->Enabled()) {
action->LogAction();
bool actionResult = false;
action->WithLock([&action, &actionResult]() {
actionResult = action->PerformAction();
});
actionsExecutedSuccessfully =
actionsExecutedSuccessfully && actionResult;
} else {
vblog(LOG_INFO, "skipping disabled action %s",
action->GetId().c_str());
}
if (!actionsExecutedSuccessfully || (_paused && !ignorePause) ||
_stop || _die) {
break;
}
if (action->Enabled()) {
action->EnableHighlight();
}
}
return actionsExecutedSuccessfully;
}
bool Macro::RunActions(bool ignorePause)
{
mblog(LOG_INFO, "running actions of %s", _name.c_str());
return RunActionsHelper(_actions, ignorePause);
}
bool Macro::RunElseActions(bool ignorePause)
{
mblog(LOG_INFO, "running else actions of %s", _name.c_str());
return RunActionsHelper(_elseActions, ignorePause);
}
bool Macro::WasPausedSince(const TimePoint &time) const
{
return _lastUnpauseTime > time;
}
void Macro::SetStopActionsIfNotDone(bool stopActionsIfNotDone)
{
_stopActionsIfNotDone = stopActionsIfNotDone;
}
void Macro::SetShortCircuitEvaluation(bool useShortCircuitEvaluation)
{
_useShortCircuitEvaluation = useShortCircuitEvaluation;
}
bool Macro::ShortCircuitEvaluationEnabled() const
{
return _useShortCircuitEvaluation;
}
void Macro::SetCustomConditionCheckIntervalEnabled(bool enable)
{
_useCustomConditionCheckInterval = enable;
}
bool Macro::CustomConditionCheckIntervalEnabled() const
{
return _useCustomConditionCheckInterval;
}
void Macro::SetCustomConditionCheckInterval(const Duration &duration)
{
_customConditionCheckInterval = duration;
}
Duration Macro::GetCustomConditionCheckInterval() const
{
return _customConditionCheckInterval;
}
void Macro::SetPaused(bool pause)
{
if (_paused && !pause) {
_lastUnpauseTime = std::chrono::high_resolution_clock::now();
ResetTimers();
}
_paused = pause;
}
void Macro::AddHelperThread(std::thread &&newThread)
{
for (unsigned int i = 0; i < _helperThreads.size(); i++) {
if (!_helperThreads[i].joinable()) {
_helperThreads[i] = std::move(newThread);
return;
}
}
_helperThreads.push_back(std::move(newThread));
}
void Macro::SetPauseStateSaveBehavior(PauseStateSaveBehavior behavior)
{
_pauseSaveBehavior = behavior;
}
Macro::PauseStateSaveBehavior Macro::GetPauseStateSaveBehavior() const
{
return _pauseSaveBehavior;
}
void Macro::Stop()
{
_stop = true;
GetMacroWaitCV().notify_all();
for (auto &t : _helperThreads) {
if (t.joinable()) {
t.join();
}
}
if (_actionRunFuture.valid()) {
_actionRunFuture.get();
}
if (_conditionCheckFuture.valid()) {
_conditionCheckFuture.get();
}
}
void Macro::SetCheckInParallel(bool parallel)
{
_checkInParallel = parallel;
_conditionCheckFuture = {};
}
bool Macro::ParallelTasksCompleted() const
{
// A parallel action run might be triggered by RunInParallel() or the
// "Run Macro" button, so checking just for RunInParallel() will not
// suffice
if (!CheckInParallel() && !_actionRunFuture.valid()) {
return true;
}
if (_actionRunFuture.valid()) {
return false;
}
if (CheckInParallel() && _conditionCheckFuture.valid()) {
return false;
}
return true;
}
MacroInputVariables Macro::GetInputVariables() const
{
return _inputVariables;
}
void Macro::SetInputVariables(const MacroInputVariables &inputVariables)
{
_inputVariables = inputVariables;
}
std::vector<TempVariable> Macro::GetTempVars(const MacroSegment *filter) const
{
std::vector<TempVariable> res;
auto addTempVars = [&res](const std::deque<std::shared_ptr<MacroSegment>>
&segments) {
for (const auto &s : segments) {
const auto &tempVars = s->_tempVariables;
res.insert(res.end(), tempVars.begin(), tempVars.end());
}
};
addTempVars({_conditions.begin(), _conditions.end()});
addTempVars({_actions.begin(), _actions.end()});
addTempVars({_elseActions.begin(), _elseActions.end()});
if (!filter) {
return res;
}
auto isCondition = [this](const MacroSegment *segment) -> bool {
return std::find_if(_conditions.begin(), _conditions.end(),
[segment](
const std::shared_ptr<MacroSegment>
&ptr) {
return ptr.get() == segment;
}) != _conditions.end();
};
auto isAction = [this](const MacroSegment *segment) -> bool {
return std::find_if(_actions.begin(), _actions.end(),
[segment](
const std::shared_ptr<MacroSegment>
&ptr) {
return ptr.get() == segment;
}) != _actions.end();
};
auto isElseAction = [this](const MacroSegment *segment) -> bool {
return std::find_if(_elseActions.begin(), _elseActions.end(),
[segment](
const std::shared_ptr<MacroSegment>
&ptr) {
return ptr.get() == segment;
}) != _elseActions.end();
};
const int filterIndex = filter->GetIndex();
// Remove all actions and else actions and conditions after filterIndex
if (isCondition(filter)) {
for (auto it = res.begin(); it != res.end();) {
auto segment = it->Segment().lock().get();
if (isCondition(segment) &&
segment->GetIndex() >= filterIndex) {
it = res.erase(it);
continue;
}
if (isAction(segment) || isElseAction(segment)) {
it = res.erase(it);
continue;
}
++it;
}
return res;
}
// Remove all else actions and actions after filterIndex
if (isAction(filter)) {
for (auto it = res.begin(); it != res.end();) {
auto segment = it->Segment().lock().get();
if (isAction(segment) &&
segment->GetIndex() >= filterIndex) {
it = res.erase(it);
continue;
}
if (isElseAction(segment)) {
it = res.erase(it);
continue;
}
++it;
}
return res;
}
// Remove all actions and elseActions after filterIndex
for (auto it = res.begin(); it != res.end();) {
auto segment = it->Segment().lock().get();
if (isElseAction(segment) &&
segment->GetIndex() >= filterIndex) {
it = res.erase(it);
continue;
}
if (isAction(segment)) {
it = res.erase(it);
continue;
}
++it;
}
return res;
}
std::optional<const TempVariable> Macro::GetTempVar(const MacroSegment *segment,
const std::string &id) const
{
if (!segment) {
return {};
}
return segment->GetTempVar(id);
}
void Macro::InvalidateTempVarValues() const
{
auto invalidateHelper =
[](const std::deque<std::shared_ptr<MacroSegment>> &segments) {
for (const auto &s : segments) {
s->InvalidateTempVarValues();
}
};
invalidateHelper({_conditions.begin(), _conditions.end()});
invalidateHelper({_actions.begin(), _actions.end()});
invalidateHelper({_elseActions.begin(), _elseActions.end()});
}
std::deque<std::shared_ptr<MacroCondition>> &Macro::Conditions()
{
return _conditions;
}
const std::deque<std::shared_ptr<MacroCondition>> &Macro::Conditions() const
{
return _conditions;
}
std::deque<std::shared_ptr<MacroAction>> &Macro::Actions()
{
return _actions;
}
const std::deque<std::shared_ptr<MacroAction>> &Macro::Actions() const
{
return _actions;
}
std::deque<std::shared_ptr<MacroAction>> &Macro::ElseActions()
{
return _elseActions;
}
const std::deque<std::shared_ptr<MacroAction>> &Macro::ElseActions() const
{
return _elseActions;
}
static void updateIndicesHelper(std::deque<std::shared_ptr<MacroSegment>> &list)
{
int idx = 0;
for (auto segment : list) {
segment->SetIndex(idx);
idx++;
}
}
void Macro::UpdateActionIndices()
{
std::deque<std::shared_ptr<MacroSegment>> list(_actions.begin(),
_actions.end());
updateIndicesHelper(list);
}
void Macro::UpdateElseActionIndices()
{
std::deque<std::shared_ptr<MacroSegment>> list(_elseActions.begin(),
_elseActions.end());
updateIndicesHelper(list);
}
void Macro::UpdateConditionIndices()
{
std::deque<std::shared_ptr<MacroSegment>> list(_conditions.begin(),
_conditions.end());
updateIndicesHelper(list);
}
std::shared_ptr<Macro> Macro::Parent() const
{
return _parent.lock();
}
bool Macro::Save(obs_data_t *obj, bool saveForCopy) const
{
if (!saveForCopy) {
obs_data_set_string(obj, "name", _name.c_str());
}
obs_data_set_bool(obj, "group", _isGroup);
if (_isGroup) {
OBSDataAutoRelease groupData = obs_data_create();
obs_data_set_bool(groupData, "collapsed", _isCollapsed);
obs_data_set_int(groupData, "size", _groupSize);
obs_data_set_obj(obj, "groupData", groupData);
return true;
}
obs_data_set_int(obj, "pauseSaveBehavior",
static_cast<int>(_pauseSaveBehavior));
obs_data_set_bool(obj, "pause", _paused);
obs_data_set_bool(obj, "parallel", _runInParallel);
obs_data_set_bool(obj, "checkConditionsInParallel", _checkInParallel);
obs_data_set_int(obj, "actionTriggerMode",
static_cast<int>(_actionTriggerMode));
obs_data_set_bool(obj, "skipExecOnStart", _skipExecOnStart);
obs_data_set_bool(obj, "stopActionsIfNotDone", _stopActionsIfNotDone);
obs_data_set_bool(obj, "useShortCircuitEvaluation",
_useShortCircuitEvaluation);
obs_data_set_bool(obj, "useCustomConditionCheckInterval",
_useCustomConditionCheckInterval);
_customConditionCheckInterval.Save(obj, "customConditionCheckInterval");
_dockSettings.Save(obj, saveForCopy);
SaveSplitterPos(_actionConditionSplitterPosition, obj,
"macroActionConditionSplitterPosition");
SaveSplitterPos(_elseActionSplitterPosition, obj,
"macroElseActionSplitterPosition");
obs_data_set_bool(obj, "registerHotkeys", _registerHotkeys);
OBSDataArrayAutoRelease pauseHotkey = obs_hotkey_save(_pauseHotkey);
obs_data_set_array(obj, "pauseHotkey", pauseHotkey);
OBSDataArrayAutoRelease unpauseHotkey = obs_hotkey_save(_unpauseHotkey);
obs_data_set_array(obj, "unpauseHotkey", unpauseHotkey);
OBSDataArrayAutoRelease togglePauseHotkey =
obs_hotkey_save(_togglePauseHotkey);
obs_data_set_array(obj, "togglePauseHotkey", togglePauseHotkey);
OBSDataArrayAutoRelease conditions = obs_data_array_create();
for (auto &c : _conditions) {
OBSDataAutoRelease arrayObj = obs_data_create();
c->Save(arrayObj);
obs_data_array_push_back(conditions, arrayObj);
}
obs_data_set_array(obj, "conditions", conditions);
OBSDataArrayAutoRelease actions = obs_data_array_create();
for (auto &a : _actions) {
OBSDataAutoRelease arrayObj = obs_data_create();
a->Save(arrayObj);
obs_data_array_push_back(actions, arrayObj);
}
obs_data_set_array(obj, "actions", actions);
OBSDataArrayAutoRelease elseActions = obs_data_array_create();
for (auto &a : _elseActions) {
OBSDataAutoRelease arrayObj = obs_data_create();
a->Save(arrayObj);
obs_data_array_push_back(elseActions, arrayObj);
}
obs_data_set_array(obj, "elseActions", elseActions);
_inputVariables.Save(obj);
return true;
}
bool Macro::Load(obs_data_t *obj)
{
_name = obs_data_get_string(obj, "name");
_isGroup = obs_data_get_bool(obj, "group");
if (_isGroup) {
OBSDataAutoRelease groupData =
obs_data_get_obj(obj, "groupData");
_isCollapsed = obs_data_get_bool(groupData, "collapsed");
_groupSize = obs_data_get_int(groupData, "size");
return true;
}
_pauseSaveBehavior = static_cast<PauseStateSaveBehavior>(
obs_data_get_int(obj, "pauseSaveBehavior"));
switch (_pauseSaveBehavior) {
case PauseStateSaveBehavior::PERSIST:
_paused = obs_data_get_bool(obj, "pause");
break;
case PauseStateSaveBehavior::PAUSE:
_paused = true;
break;
case PauseStateSaveBehavior::UNPAUSE:
_paused = false;
break;
default:
_paused = obs_data_get_bool(obj, "pause");
break;
}
_runInParallel = obs_data_get_bool(obj, "parallel");
_checkInParallel = obs_data_get_bool(obj, "checkConditionsInParallel");
if (obs_data_has_user_value(obj, "onChange")) {
const bool onChange = obs_data_get_bool(obj, "onChange");
_actionTriggerMode =
onChange ? ActionTriggerMode::MACRO_RESULT_CHANGED
: ActionTriggerMode::ALWAYS;
} else {
_actionTriggerMode = static_cast<ActionTriggerMode>(
obs_data_get_int(obj, "actionTriggerMode"));
}
_skipExecOnStart = obs_data_get_bool(obj, "skipExecOnStart");
_stopActionsIfNotDone = obs_data_get_bool(obj, "stopActionsIfNotDone");
_useShortCircuitEvaluation =
obs_data_get_bool(obj, "useShortCircuitEvaluation");
_useCustomConditionCheckInterval =
obs_data_get_bool(obj, "useCustomConditionCheckInterval");
_customConditionCheckInterval.Load(obj, "customConditionCheckInterval");
_dockSettings.Load(obj);
LoadSplitterPos(_actionConditionSplitterPosition, obj,
"macroActionConditionSplitterPosition");
LoadSplitterPos(_elseActionSplitterPosition, obj,
"macroElseActionSplitterPosition");
obs_data_set_default_bool(obj, "registerHotkeys", true);
_registerHotkeys = obs_data_get_bool(obj, "registerHotkeys");
if (_registerHotkeys) {
SetupHotkeys();
}
OBSDataArrayAutoRelease pauseHotkey =
obs_data_get_array(obj, "pauseHotkey");
obs_hotkey_load(_pauseHotkey, pauseHotkey);
OBSDataArrayAutoRelease unpauseHotkey =
obs_data_get_array(obj, "unpauseHotkey");
obs_hotkey_load(_unpauseHotkey, unpauseHotkey);
OBSDataArrayAutoRelease togglePauseHotkey =
obs_data_get_array(obj, "togglePauseHotkey");
obs_hotkey_load(_togglePauseHotkey, togglePauseHotkey);
SetHotkeysDesc();
bool root = true;
OBSDataArrayAutoRelease conditions =
obs_data_get_array(obj, "conditions");
size_t count = obs_data_array_count(conditions);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj =
obs_data_array_item(conditions, i);
std::string id = obs_data_get_string(arrayObj, "id");
auto newEntry = MacroConditionFactory::Create(id, this);
if (newEntry) {
_conditions.emplace_back(newEntry);
auto condition = _conditions.back().get();
condition->WithLock([&]() {
condition->Load(arrayObj);
condition->ValidateLogicSelection(
root, Name().c_str());
});
} else {
blog(LOG_WARNING,
"discarding condition entry with unknown id (%s) for macro %s",
id.c_str(), _name.c_str());
}
root = false;
}
UpdateConditionIndices();
OBSDataArrayAutoRelease actions = obs_data_get_array(obj, "actions");
count = obs_data_array_count(actions);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(actions, i);
std::string id = obs_data_get_string(arrayObj, "id");
auto newEntry = MacroActionFactory::Create(id, this);
if (newEntry) {
_actions.emplace_back(newEntry);
auto action = _actions.back().get();
action->WithLock([action, &arrayObj]() {
action->Load(arrayObj);
});
} else {
blog(LOG_WARNING,
"discarding action entry with unknown id (%s) for macro %s",
id.c_str(), _name.c_str());
}
}
UpdateActionIndices();
OBSDataArrayAutoRelease elseActions =
obs_data_get_array(obj, "elseActions");
count = obs_data_array_count(elseActions);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj =
obs_data_array_item(elseActions, i);
std::string id = obs_data_get_string(arrayObj, "id");
auto newEntry = MacroActionFactory::Create(id, this);
if (newEntry) {
_elseActions.emplace_back(newEntry);
auto action = _elseActions.back().get();
action->WithLock([action, &arrayObj]() {
action->Load(arrayObj);
});
} else {
blog(LOG_WARNING,
"discarding elseAction entry with unknown id (%s) for macro %s",
id.c_str(), _name.c_str());
}
}
UpdateElseActionIndices();
_inputVariables.Load(obj);
return true;
}
bool Macro::PostLoad()
{
for (auto &c : _conditions) {
c->WithLock([c]() { c->PostLoad(); });
}
for (auto &a : _actions) {
a->WithLock([a]() { a->PostLoad(); });
}
for (auto &a : _elseActions) {
a->WithLock([a]() { a->PostLoad(); });
}
return true;
}
bool Macro::SwitchesScene() const
{
for (const auto &a : _actions) {
if (a->GetId() == MacroAction::GetDefaultID()) {
return true;
}
}
for (const auto &a : _elseActions) {
if (a->GetId() == MacroAction::GetDefaultID()) {
return true;
}
}
return false;
}
const QList<int> &Macro::GetActionConditionSplitterPosition() const
{
return _actionConditionSplitterPosition;
}
void Macro::SetActionConditionSplitterPosition(const QList<int> sizes)
{
_actionConditionSplitterPosition = sizes;
}
const QList<int> &Macro::GetElseActionSplitterPosition() const
{
return _elseActionSplitterPosition;
}
void Macro::SetElseActionSplitterPosition(const QList<int> sizes)
{
_elseActionSplitterPosition = sizes;
}
bool Macro::HasValidSplitterPositions() const
{
return !_actionConditionSplitterPosition.empty() &&
!_elseActionSplitterPosition.empty();
}
void Macro::ResetUIHelpers()
{
for (auto c : _conditions) {
c->GetHighlightAndReset();
}
for (auto a : _actions) {
a->GetHighlightAndReset();
}
}
void Macro::EnablePauseHotkeys(bool value)
{
if (_registerHotkeys == value) {
return;
}
if (_registerHotkeys) {
ClearHotkeys();
} else {
SetupHotkeys();
}
_registerHotkeys = value;
}
bool Macro::PauseHotkeysEnabled() const
{
return _registerHotkeys;
}
static void pauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed)
{
if (pressed) {
auto m = static_cast<Macro *>(data);
m->SetPaused(true);
}
}
static void unpauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed)
{
if (pressed) {
auto m = static_cast<Macro *>(data);
m->SetPaused(false);
}
}
static void togglePauseCB(void *data, obs_hotkey_id, obs_hotkey_t *,
bool pressed)
{
if (pressed) {
auto m = static_cast<Macro *>(data);
m->SetPaused(!m->Paused());
}
}
static int macroHotkeyID = 0;
static obs_hotkey_id registerHotkeyHelper(const std::string prefix,
const char *formatModuleText,
Macro *macro, obs_hotkey_func func)
{
macroHotkeyID++;
std::string hotkeyName = prefix + std::to_string(macroHotkeyID);
QString format{obs_module_text(formatModuleText)};
QString hotkeyDesc = format.arg(QString::fromStdString(macro->Name()));
return obs_hotkey_register_frontend(hotkeyName.c_str(),
hotkeyDesc.toStdString().c_str(),
func, macro);
}
void Macro::SetupHotkeys()
{
if (_pauseHotkey != OBS_INVALID_HOTKEY_ID ||
_unpauseHotkey != OBS_INVALID_HOTKEY_ID ||
_togglePauseHotkey != OBS_INVALID_HOTKEY_ID) {
ClearHotkeys();
}
_pauseHotkey = registerHotkeyHelper(
"macro_pause_hotkey_", "AdvSceneSwitcher.hotkey.macro.pause",
this, pauseCB);
_unpauseHotkey = registerHotkeyHelper(
"macro_unpause_hotkey_",
"AdvSceneSwitcher.hotkey.macro.unpause", this, unpauseCB);
_togglePauseHotkey = registerHotkeyHelper(
"macro_toggle_pause_hotkey_",
"AdvSceneSwitcher.hotkey.macro.togglePause", this,
togglePauseCB);
}
void Macro::ClearHotkeys() const
{
obs_hotkey_unregister(_pauseHotkey);
obs_hotkey_unregister(_unpauseHotkey);
obs_hotkey_unregister(_togglePauseHotkey);
}
void setHotkeyDescriptionHelper(const char *formatModuleText,
const std::string name, const obs_hotkey_id id)
{
#ifndef UNIT_TEST
QString format{obs_module_text(formatModuleText)};
QString hotkeyDesc = format.arg(QString::fromStdString(name));
obs_hotkey_set_description(id, hotkeyDesc.toStdString().c_str());
#endif // !UNIT_TEST
}
void Macro::SetHotkeysDesc() const
{
setHotkeyDescriptionHelper("AdvSceneSwitcher.hotkey.macro.pause", _name,
_pauseHotkey);
setHotkeyDescriptionHelper("AdvSceneSwitcher.hotkey.macro.unpause",
_name, _unpauseHotkey);
setHotkeyDescriptionHelper("AdvSceneSwitcher.hotkey.macro.togglePause",
_name, _togglePauseHotkey);
}
void SaveMacros(obs_data_t *obj)
{
obs_data_array_t *macroArray = obs_data_array_create();
for (const auto &m : GetTopLevelMacros()) {
obs_data_t *array_obj = obs_data_create();
m->Save(array_obj);
obs_data_array_push_back(macroArray, array_obj);
obs_data_release(array_obj);
}
obs_data_set_array(obj, "macros", macroArray);
obs_data_array_release(macroArray);
}
void LoadMacros(obs_data_t *obj)
{
auto &macros = GetTopLevelMacros();
macros.clear();
obs_data_array_t *macroArray = obs_data_get_array(obj, "macros");
size_t count = obs_data_array_count(macroArray);
for (size_t i = 0; i < count; i++) {
obs_data_t *array_obj = obs_data_array_item(macroArray, i);
macros.emplace_back(std::make_shared<Macro>());
macros.back()->Load(array_obj);
obs_data_release(array_obj);
}
obs_data_array_release(macroArray);
int groupCount = 0;
std::shared_ptr<Macro> group;
std::vector<std::shared_ptr<Macro>> invalidGroups;
for (const auto &m : macros) {
if (groupCount && m->IsGroup()) {
blog(LOG_ERROR,
"nested group detected - will delete \"%s\"",
m->Name().c_str());
invalidGroups.emplace_back(m);
continue;
}
if (groupCount) {
m->SetParent(group);
groupCount--;
}
if (m->IsGroup()) {
groupCount = m->GroupSize();
group = m;
}
m->PostLoad();
}
if (groupCount) {
blog(LOG_ERROR,
"invalid group size detected - will delete \"%s\"",
group->Name().c_str());
invalidGroups.emplace_back(group);
}
for (auto &m : invalidGroups) {
auto it = std::find(macros.begin(), macros.end(), m);
if (it == macros.end()) {
continue;
}
macros.erase(it);
}
}
bool CheckMacros()
{
bool matchFound = false;
for (const auto &m : GetTopLevelMacros()) {
if (!m->ConditionsShouldBeChecked()) {
vblog(LOG_INFO,
"skipping condition check for macro \"%s\" "
"(custom check interval)",
m->Name().c_str());
continue;
}
if (m->CheckConditions() || m->ElseActions().size() > 0) {
matchFound = true;
// This has to be performed here for now as actions are
// not performed immediately after checking conditions.
if (m->SwitchesScene()) {
SetMacroSwitchedScene(true);
}
}
}
return matchFound;
}
bool RunMacros()
{
// Create copy of macro list as elements might be removed, inserted, or
// reordered while macros are currently being executed.
// For example, this can happen if a macro is performing a wait action,
// as the main lock will be unlocked during this time.
auto runPhaseMacros = GetTopLevelMacros();
// Avoid deadlocks when opening settings window and calling frontend
// API functions at the same time.
//
// If the timing is just right, the frontend API call will call
// QMetaObject::invokeMethod(...) with Qt::BlockingQueuedConnection
// while holding the main switcher mutex.
// But this invokeMethod call itself will be blocked as it is waiting
// the constructor of AdvSceneSwitcher() to complete.
// The constructor of AdvSceneSwitcher() cannot continue however as it
// cannot lock the main switcher mutex.
auto lock = GetLoopLock();
if (lock) {
lock->unlock();
}
for (auto &m : runPhaseMacros) {
if (!m || !m->ShouldRunActions()) {
continue;
}
if (IsFirstInterval() && m->SkipExecOnStart()) {
blog(LOG_INFO,
"skip execution of macro \"%s\" at startup",
m->Name().c_str());
continue;
}
vblog(LOG_INFO, "running macro: %s", m->Name().c_str());
if (!m->PerformActions(m->ConditionsMatched())) {
blog(LOG_WARNING, "abort macro: %s", m->Name().c_str());
}
}
if (lock) {
lock->lock();
}
return true;
}
void StopAllMacros()
{
for (const auto &m : GetAllMacros()) {
m->Stop();
}
}
void InvalidateMacroTempVarValues()
{
for (const auto &m : GetTopLevelMacros()) {
// Do not invalidate the temp vars set during condition checks
// or action executions running in parallel to the "main" macro
// loop, as otherwise access to the information stored in those
// variables might get lost while those checks or actions are
// still ongoing.
if (!m->ParallelTasksCompleted()) {
continue;
}
m->InvalidateTempVarValues();
}
}
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval()
{
auto &macros = GetTopLevelMacros();
if (macros.empty()) {
return {};
}
for (const auto &macro : macros) {
if (!macro) {
continue;
}
if (!macro->CustomConditionCheckIntervalEnabled()) {
continue;
}
if (macro->GetCustomConditionCheckInterval().Milliseconds() <
GetIntervalValue()) {
return macro;
}
}
return {};
}
} // namespace advss