424 lines
9.8 KiB
C++
Executable File
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
|