Files
qt5-legatus-legacy/Lts/Services/AreaEffectSimulation/Utils/fire_automaton.cpp
T
2026-08-13 18:33:10 +02:00

424 lines
9.8 KiB
C++
Executable File

/**
* AES fire automaton - scheduling fire patterns for multiple effectors
* $Id$
* $HeadURL$
* $Author$
*/
#include <QtGlobal>
#include <QTimer>
#include <QStringList>
#include <QVector>
#include <AreaEffectSimulation/logger.h>
#include <AreaEffectSimulation/settings.h>
#include "fire_automaton.h"
namespace AreaEffectSimulation
{
FireAutomaton::FireAutomaton(QObject *parent)
: QObject(parent)
, m_nextIdx(0)
, m_tick(0)
, m_durationMSecs(0)
, m_intervalMSecs(0)
, m_remainingTickMSecs(0)
, m_state(State::Uninitialized)
, m_logger(getLogger("Utils.FireAutomaton"))
{
m_timer.setSingleShot(true);
connect(&m_timer, &QTimer::timeout, this, &FireAutomaton::nextTick, Qt::UniqueConnection);
}
/**
* @brief Initializes the fire automaton and derives a fire sequence from given parameters
*
* @param time Time used for the fire sequence calculation in milliseconds
* @param timeType Type of the given time
* @param rounds Number of rounds to be fired
* @param effectorIds Ids of the effector that shall be used to fire rounds
* @param pattern Fire pattern to be used
* @return true when the automaton is succesfully initialized
*/
bool FireAutomaton::init(int32_t time, TimeType type, int32_t rounds, const QVector<int32_t>& effectorIds, const QString& pattern)
{
if(m_state != State::Stopped && m_state != State::Uninitialized)
{
ERROR_LOG(m_logger) << " init failed - fire automaton must be in state unitialized or stopped.";
return false;
}
if(time < 0)
{
ERROR_LOG(m_logger) << " init failed - time invalid.";
return false;
}
if(effectorIds.isEmpty())
{
ERROR_LOG(m_logger) << " init failed - effector Ids invalid.";
return false;
}
if(rounds <= 0)
{
ERROR_LOG(m_logger) << " init failed - number of rounds invalid.";
return false;
}
if(pattern.isEmpty())
{
ERROR_LOG(m_logger) << " init failed - invalid fire pattern.";
return false;
}
m_effectorIds = effectorIds;
m_tick = 0;
m_remainingTickMSecs = 0;
m_nextIdx = 0;
if(!readPattern(pattern))
{
ERROR_LOG(m_logger) << " init failed - firing pattern invalid.";
return false;
}
if(!calculateSchedule(rounds))
{
ERROR_LOG(m_logger) << " init failed - firing schedule invalid.";
return false;
}
const int32_t maxTick = m_schedule.size();
switch(type)
{
case TimeType::Cadence:
m_intervalMSecs = time;
m_durationMSecs = (maxTick - 1) * time;
break;
case TimeType::Duration:
m_durationMSecs = time;
if(maxTick == 1)
{
m_intervalMSecs = 0;
}
else
{
m_intervalMSecs = m_durationMSecs / (maxTick - 1);
// ensure atleast 1 ms spacing between ticks
if(m_intervalMSecs <= 0)
{
m_intervalMSecs = 1;
}
}
break;
}
setState(State::Stopped);
return true;
}
void FireAutomaton::setState(State state)
{
m_state = state;
}
void FireAutomaton::nextTick()
{
QVector<int32_t> fireIds = getEffectorIds(m_tick);
for(auto id : fireIds)
{
emit fire(id);
}
++m_tick;
if(m_tick >= m_schedule.size())
{
stop();
emit finished();
return;
}
if(m_state != State::Stepping)
{
m_timer.start(m_intervalMSecs);
}
}
/**
* @brief Starts the fire automaton's sequence derived from the fire pattern and parameters
*
* The sequence can only be started in state stopped and sets the state to running. After the sequence is finished, the state will be set to stopped.
*
* @return True when the automaton is succesfully resumed
*/
bool FireAutomaton::start()
{
if(m_state != State::Stopped)
{
ERROR_LOG(m_logger) << " start failed - fire automaton must be in state stopped.";
return false;
}
setState(State::Running);
m_timer.start(0);
return true;
}
/**
* @brief Resumes a previously stopped fire automaton
*
* In the state paused the fire automaton sequence execution will be resumed and the state set to running.
*
* @return True when the automaton is succesfully resumed
*/
bool FireAutomaton::resume()
{
if(m_state != State::Paused)
{
ERROR_LOG(m_logger) << " resume failed - fire automaton must be in state paused.";
return false;
}
setState(State::Running);
m_timer.start(m_remainingTickMSecs);
return true;
}
/**
* @brief Pauses a running fire automaton sequence
*
* In the state running the fire automaton sequence execution will be halted and the state set to paused.
*
* @return True when the automaton is succesfully paused
*/
bool FireAutomaton::pause()
{
if(m_state != State::Running)
{
ERROR_LOG(m_logger) << " pause failed - fire automaton must be in state running.";
return false;
}
m_remainingTickMSecs = m_timer.remainingTime();
m_timer.stop();
setState(State::Paused);
return true;
}
/**
* @brief Stops and resets the fire automaton
*
* Forces the state stopped and resets the automaton sequence execution.
*/
void FireAutomaton::stop()
{
m_tick = 0;
m_remainingTickMSecs = 0;
m_timer.stop();
setState(State::Stopped);
}
/**
* @brief Steps through the derived fire automaton sequence
*
* Stepping can only be started from the state stopped and sets the state to stepping. After stepping through the whole sequence, the state will be set to stopped.
*
* @return True when step is succesfully performed
*/
bool FireAutomaton::performStep()
{
if(m_state != State::Stopped && m_state != State::Stepping)
{
ERROR_LOG(m_logger) << " performStep failed - fire automaton must be in state stepping or stopped.";
return false;
}
m_state = State::Stepping;
nextTick();
return true;
}
/**
* @brief Returns the current state of the fire automaton
*
* @return Fire automaton state
*/
FireAutomaton::State FireAutomaton::getState() const
{
return m_state;
}
bool FireAutomaton::readPattern(const QString& pattern)
{
bool fill = false;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList elements = pattern.split(QStringLiteral(","), Qt::SkipEmptyParts);
#else
QStringList elements = pattern.split(QStringLiteral(","), QString::SkipEmptyParts);
#endif
for(const QString& element : elements)
{
FireAutomaton::PatternElement patternElement;
if(!patternElementFromString(element, patternElement))
{
WARNING_LOG(m_logger) << " skipped invalid firing pattern elements.";
continue;
}
if(patternElement == FireAutomaton::PatternElement::NextFill || patternElement == FireAutomaton::PatternElement::AllFill)
{
if(!fill)
{
fill = true;
}
else
{
ERROR_LOG(m_logger) << " a pattern may contain at most one fill element.";
return false;
}
}
m_pattern.append(patternElement);
}
if(m_pattern.empty())
{
ERROR_LOG(m_logger) << " error occured reading firing pattern.";
return false;
}
return true;
}
bool FireAutomaton::patternElementFromString(const QString& str, FireAutomaton::PatternElement& out)
{
const QString key = str.trimmed().toLower();
if(key == QStringLiteral("all"))
out = FireAutomaton::PatternElement::All;
else if(key == QStringLiteral("all*"))
out = FireAutomaton::PatternElement::AllFill;
else if(key == QStringLiteral("next"))
out = FireAutomaton::PatternElement::Next;
else if(key == QStringLiteral("next*"))
out = FireAutomaton::PatternElement::NextFill;
else if(key == QStringLiteral("idle"))
out = FireAutomaton::PatternElement::Idle;
else
return false;
return true;
}
QVector<int32_t> FireAutomaton::getEffectorIds(int32_t tick) const
{
return m_schedule[tick];
}
int32_t FireAutomaton::getMaxTick() const
{
return m_schedule.size();
}
bool FireAutomaton::calculateSchedule(int32_t rounds)
{
int32_t minCounter = 0;
// count needed rounds
for(const FireAutomaton::PatternElement& element : m_pattern)
{
if(element == FireAutomaton::PatternElement::All)
minCounter += m_effectorIds.size();
else if(element == FireAutomaton::PatternElement::Next)
++minCounter;
}
int32_t fillCounter = rounds - minCounter;
m_schedule.clear();
for(const FireAutomaton::PatternElement& element : m_pattern)
{
// stop when all rounds used
if(rounds <= 0)
break;
if(element == FireAutomaton::PatternElement::All)
{
if(rounds >= m_effectorIds.size())
{
m_schedule.append(m_effectorIds);
rounds -= m_effectorIds.size();
}
else
{
m_schedule.append(m_effectorIds.mid(0,rounds));
rounds = 0;
}
}
else if(element == FireAutomaton::PatternElement::AllFill && fillCounter > 0)
{
const int32_t times = fillCounter / m_effectorIds.size();
const int32_t remainder = fillCounter % m_effectorIds.size();
for(int32_t i=0; i < times; i++)
{
m_schedule.append(m_effectorIds);
rounds -= m_effectorIds.size();
}
if(remainder > 0)
{
m_schedule.append(m_effectorIds.mid(0, remainder));
rounds -= remainder;
}
}
else if(element == FireAutomaton::PatternElement::Idle)
{
m_schedule.append(QVector<int32_t>{});
}
else if(element == FireAutomaton::PatternElement::Next)
{
m_schedule.append(QVector<int32_t>{m_effectorIds[m_nextIdx]});
m_nextIdx = (m_nextIdx + 1) % m_effectorIds.size();
rounds--;
}
else if(element == FireAutomaton::PatternElement::NextFill && fillCounter > 0)
{
for(int32_t i = 0; i < fillCounter; i++)
{
m_schedule.append(QVector<int32_t>{m_effectorIds[i % m_effectorIds.size()]});
rounds--;
}
}
}
if(rounds > 0)
{
ERROR_LOG(m_logger) << " scheduling error - fire pattern does not provide sufficient slots to schedule all rounds. " << rounds;
return false;
}
return true;
}
} // namespace AreaEffectSimulation