SceneSwitcher/lib/macro/macro.cpp

1298 lines
33 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->CheckCondition();
});
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);
_conditionSateChanged = _lastMatched != _matched;
const bool hasActionsToExecute = _matched ? (_actions.size() > 0)
: (_elseActions.size() > 0);
if (!_conditionSateChanged && _performActionsOnChange &&
hasActionsToExecute) {
_lastOnChangeActionsPreventedTime =
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::OnChangePreventedActionsSince(const TimePoint &time) const
{
return _lastOnChangeActionsPreventedTime > time;
}
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) &&
(!_performActionsOnChange || _conditionSateChanged);
if (VerboseLoggingEnabled() && _performActionsOnChange &&
!_conditionSateChanged) {
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 = {};
}
bool Macro::RunActionsHelper(
const std::deque<std::shared_ptr<MacroAction>> &actionsToRun,
bool ignorePause)
{
// 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::SetMatchOnChange(bool onChange)
{
_performActionsOnChange = onChange;
}
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_bool(obj, "onChange", _performActionsOnChange);
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");
_performActionsOnChange = obs_data_get_bool(obj, "onChange");
_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)
{
QString format{obs_module_text(formatModuleText)};
QString hotkeyDesc = format.arg(QString::fromStdString(name));
obs_hotkey_set_description(id, hotkeyDesc.toStdString().c_str());
}
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