#include "macro.hpp" #include "condition-logic.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 #include #include #include #include #include #undef max #include namespace advss { static bool setup() { AddPluginCleanupStep([]() { GetTopLevelMacros().clear(); GetTemporaryMacros().clear(); }); return true; } static bool setupDone = setup(); 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::CreateGroup(const std::string &name, std::vector> &children) { auto group = std::make_shared(name); group->_registerHotkeys = false; group->_isGroup = true; if (!children.empty()) { const auto parent = children.front()->Parent(); const bool haveSameParent = std::all_of( children.begin(), children.end(), [&parent](const std::shared_ptr &child) { return child->Parent() == parent; }); if (haveSameParent) { PrepareMoveToGroup(parent, group); } } for (auto &child : children) { PrepareMoveToGroup(group, child); } return group; } void Macro::RemoveGroup(std::shared_ptr group) { auto ¯os = GetTopLevelMacros(); auto it = std::find(macros.begin(), macros.end(), group); if (it == macros.end()) { return; } const auto parent = group->Parent(); for (const auto &child : GetGroupChildren(group.get())) { child->SetParent(parent); } AddToGroupSizes(parent, -1); macros.erase(it); } void Macro::PrepareMoveToGroup(Macro *group, std::shared_ptr item) { auto ¯os = GetTopLevelMacros(); for (const auto &m : macros) { if (m.get() == group) { PrepareMoveToGroup(m, item); return; } } PrepareMoveToGroup(std::shared_ptr(), item); } void Macro::PrepareMoveToGroup(std::shared_ptr group, std::shared_ptr item) { if (!item) { return; } if (group && (group == item || IsMacroInGroup(group.get(), item.get()))) { blog(LOG_WARNING, "cannot move \"%s\" into itself", item->Name().c_str()); return; } const int count = 1 + (int)item->_groupSize; AddToGroupSizes(item->Parent(), -count); item->SetParent(group); AddToGroupSizes(group, count); } void Macro::AddToGroupSizes(std::shared_ptr group, int count) { for (; group; group = group->Parent()) { assert((int)group->_groupSize + count >= 0); group->_groupSize += count; } } void Macro::RestoreGroupHierarchy( const std::deque> ¯os) { struct OpenGroup { std::shared_ptr group; uint32_t remaining; }; std::vector openGroups; for (size_t i = 0; i < macros.size(); i++) { const auto ¯o = macros[i]; while (!openGroups.empty() && openGroups.back().remaining == 0) { openGroups.pop_back(); } macro->SetParent(openGroups.empty() ? nullptr : openGroups.back().group); for (auto &openGroup : openGroups) { openGroup.remaining--; } if (!macro->IsGroup()) { continue; } const auto maxSize = openGroups.empty() ? (uint32_t)(macros.size() - i - 1) : openGroups.back().remaining; auto size = macro->_groupSize; if (size > maxSize) { blog(LOG_WARNING, "invalid group size detected for \"%s\" - " "reducing size from %u to %u", macro->Name().c_str(), size, maxSize); size = maxSize; } openGroups.push_back({macro, size}); } for (const auto ¯o : macros) { macro->_groupSize = 0; } for (const auto ¯o : macros) { AddToGroupSizes(macro->Parent(), 1); } } static bool checkCondition(const std::shared_ptr &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(); }); condition->ApplyVarMappings(); const auto endTime = std::chrono::high_resolution_clock::now(); const auto timeSpent = endTime - startTime; if (timeSpent >= perfLogThreshold) { blog(LOG_WARNING, "spent %lld ms in %s condition check of macro '%s'!", (long long)std::chrono::duration_cast< std::chrono::milliseconds>(timeSpent) .count(), condition->GetId().c_str(), condition->GetMacro()->Name().c_str()); } return conditionMatched; } bool Macro::CheckConditionHelper( const std::shared_ptr &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; }; if (!condition->Enabled()) { vblog(LOG_INFO, "ignoring condition '%s' for '%s'", condition->GetId().c_str(), _name.c_str()); if (!_useShortCircuitEvaluation) { (void)evaluateCondition(); } return _matched; } 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> &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 && !_paused) { _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 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(); for (auto group = _parent.lock(); group; group = group->Parent()) { group->_lastExecutionTime = _lastExecutionTime; } if (_runCount != std::numeric_limits::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( 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 && !_paused) { 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> &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(); }); action->ApplyVarMappings(); 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::SignalStop() { _stop = true; GetMacroWaitCV().notify_all(); } 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 Macro::GetTempVars(const MacroSegment *filter) const { std::vector res; auto addTempVars = [&res](const std::deque> &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 &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 &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 &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 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> &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> &Macro::Conditions() { return _conditions; } const std::deque> &Macro::Conditions() const { return _conditions; } std::deque> &Macro::Actions() { return _actions; } const std::deque> &Macro::Actions() const { return _actions; } std::deque> &Macro::ElseActions() { return _elseActions; } const std::deque> &Macro::ElseActions() const { return _elseActions; } static void updateIndicesHelper(std::deque> &list) { int idx = 0; for (auto segment : list) { segment->SetIndex(idx); idx++; } } void Macro::UpdateActionIndices() { std::deque> list(_actions.begin(), _actions.end()); updateIndicesHelper(list); } void Macro::UpdateElseActionIndices() { std::deque> list(_elseActions.begin(), _elseActions.end()); updateIndicesHelper(list); } void Macro::UpdateConditionIndices() { std::deque> list(_conditions.begin(), _conditions.end()); updateIndicesHelper(list); } std::shared_ptr Macro::Parent() const { return _parent.lock(); } std::shared_ptr Macro::CreateNested() { auto macro = std::make_shared(); macro->_isNested = true; macro->_registerHotkeys = false; return macro; } void Macro::SetNestedParentMacro(Macro *parent) { _nestedParentMacro = parent; } 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(_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(_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( 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( 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 = !_isNested && obs_data_get_bool(obj, "registerHotkeys"); _pauseHotkeyData = obs_data_get_array(obj, "pauseHotkey"); _unpauseHotkeyData = obs_data_get_array(obj, "unpauseHotkey"); _togglePauseHotkeyData = obs_data_get_array(obj, "togglePauseHotkey"); _hotkeyLoadPending = true; 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; } void Macro::LoadHotkeys() { if (!_hotkeyLoadPending) { return; } _hotkeyLoadPending = false; if (_registerHotkeys) { SetupHotkeys(); } obs_hotkey_load(_pauseHotkey, _pauseHotkeyData); obs_hotkey_load(_unpauseHotkey, _unpauseHotkeyData); obs_hotkey_load(_togglePauseHotkey, _togglePauseHotkeyData); SetHotkeysDesc(); _pauseHotkeyData = nullptr; _unpauseHotkeyData = nullptr; _togglePauseHotkeyData = nullptr; } bool Macro::PostLoad() { LoadHotkeys(); 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 &Macro::GetActionConditionSplitterPosition() const { return _actionConditionSplitterPosition; } void Macro::SetActionConditionSplitterPosition(const QList sizes) { _actionConditionSplitterPosition = sizes; } const QList &Macro::GetElseActionSplitterPosition() const { return _elseActionSplitterPosition; } void Macro::SetElseActionSplitterPosition(const QList 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(data); m->SetPaused(true); } } static void unpauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { if (pressed) { auto m = static_cast(data); m->SetPaused(false); } } static void togglePauseCB(void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { if (pressed) { auto m = static_cast(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 (_isNested) { return; } 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) { OBSDataArrayAutoRelease macroArray = obs_data_array_create(); for (const auto &m : GetTopLevelMacros()) { OBSDataAutoRelease array_obj = obs_data_create(); m->Save(array_obj); obs_data_array_push_back(macroArray, array_obj); } obs_data_set_array(obj, "macros", macroArray); } void LoadMacros(obs_data_t *obj) { auto ¯os = GetTopLevelMacros(); macros.clear(); OBSDataArrayAutoRelease macroArray = obs_data_get_array(obj, "macros"); size_t count = obs_data_array_count(macroArray); for (size_t i = 0; i < count; i++) { OBSDataAutoRelease array_obj = obs_data_array_item(macroArray, i); macros.emplace_back(std::make_shared()); macros.back()->Load(array_obj); } Macro::RestoreGroupHierarchy(macros); for (const auto &m : macros) { m->PostLoad(); } } std::vector> LoadImportedMacros( obs_data_array_t *array, const std::function &)> &shouldImport) { std::deque> loaded; auto &tempMacros = GetTemporaryMacros(); const size_t count = obs_data_array_count(array); for (size_t i = 0; i < count; i++) { tempMacros.clear(); OBSDataAutoRelease obj = obs_data_array_item(array, i); auto macro = std::make_shared(); tempMacros.emplace_back(macro); macro->Load(obj); RunAndClearPostLoadSteps(); loaded.emplace_back(macro); } tempMacros.clear(); Macro::RestoreGroupHierarchy(loaded); std::vector> imported; for (const auto ¯o : loaded) { if (!shouldImport(macro)) { for (const auto &child : loaded) { if (child->Parent() == macro) { Macro::PrepareMoveToGroup( macro->Parent(), child); } } Macro::PrepareMoveToGroup(nullptr, macro); continue; } GetTopLevelMacros().emplace_back(macro); imported.emplace_back(macro); } for (const auto ¯o : imported) { macro->PostLoad(); } RunAndClearPostLoadSteps(); return imported; } 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 SignalStopAllMacros() { for (const auto &m : GetAllMacros()) { m->SignalStop(); } } void WaitForAllMacros() { 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 GetMacroWithInvalidConditionInterval() { auto ¯os = GetTopLevelMacros(); if (macros.empty()) { return {}; } for (const auto ¯o : macros) { if (!macro) { continue; } if (!macro->CustomConditionCheckIntervalEnabled()) { continue; } if (macro->GetCustomConditionCheckInterval().Milliseconds() < GetIntervalValue()) { return macro; } } return {}; } } // namespace advss