#include "../headers/scstwrace.h" ScStwRace::ScStwRace(QObject *parent) : QObject(parent) { this->state = IDLE; // configure the loop that waits for the sound effect to finish this->soundPlayer = new ScStwSoundPlayer(); // configure timer that handles the delay between the start commands this->nextActionTimer = new QTimer(this); nextActionTimer->setSingleShot(true); this->nextActionLoop = new QEventLoop(); this->nextStartAction = None; connect(this->nextActionTimer, &QTimer::timeout, this->nextActionLoop, &QEventLoop::quit); connect(this, &ScStwRace::nextStartActionChanged, this, &ScStwRace::nextStartActionDetailsChanged); // write default settings this->startActionSettings.insert(Start, {{"Enabled", true}, {"Delay", 1}}); this->writeStartActionSetting(AtYourMarks, false, 0); this->writeStartActionSetting(Ready, false, 0); this->setSoundVolume(1.0); } // -------------------------- // --- Main Functionality --- // -------------------------- /** * Function to start the race * * @brief ScStwRace::startRace * @return {int} 200: OK; 904: MAIN state not matching */ int ScStwRace::start() { if(this->state != IDLE) { return 904; } qDebug() << "+ [INFO] starting race"; this->setState(STARTING); this->playSoundsAndStartTimers(None); return 200; } /** * Function to stop the currently running race * * @brief ScStwRace::stopRace * @param {int} type 0: stop; 1: cancel; 2: false start * @return {int} 200: OK; 904: MAIN state not matching */ int ScStwRace::stop() { if(this->state != RUNNING && this->state != STARTING) { return 904; } qDebug() << "+ [INFO] stopping race"; double timeOfStop = QDateTime::currentMSecsSinceEpoch(); int returnCode = 900; bool stopOk = true; foreach(ScStwTimer *speedTimer, this->timers){ if(!speedTimer->stop(timeOfStop) && speedTimer->getState() != ScStwTimer::DISABLED){ stopOk = false; } } returnCode = stopOk ? 200:904; if(returnCode == 200) { this->setState(STOPPED); } return returnCode; } /** * @brief ScStwRace::handleTimerStop Function to declare the winner and looser timers after a timer has been stopped */ void ScStwRace::handleTimerStop() { if(this->state == RUNNING) { // find out which timer has won double lowestStoppedTime = -1; QList timersWhichHaveWonIds; // iterate through all timers and find the lowest time taht was stopped foreach(ScStwTimer * timer, this->timers) { if(timer->getCurrentTime() > 0 && (timer->getCurrentTime() <= lowestStoppedTime || lowestStoppedTime < 0)) { // this is the timer with the lowest stopped time lowestStoppedTime = timer->getCurrentTime(); } } // append the timer(s) with the lowest stopped time to the winner list foreach(ScStwTimer * timer, this->timers) { if(timer->getCurrentTime() > 0 && (timer->getCurrentTime() <= lowestStoppedTime || lowestStoppedTime < 0) && timer->getState() != ScStwTimer::RUNNING ) { // this is the timer with the lowest stopped time timersWhichHaveWonIds.append(timer); } } // update the states of all timers foreach(ScStwTimer * timer, this->timers) { if(timer->getState() == ScStwTimer::RUNNING) continue; if(timersWhichHaveWonIds.contains(timer)) { timer->setResult(ScStwTimer::WON); } else { timer->setResult(ScStwTimer::LOST); } } } } /** * @brief ScStwRace::resetRace * @return {int} 200: OK; 904: MAIN state not matching */ int ScStwRace::reset() { if(this->state != STOPPED) { return 904; } qDebug() << "+ [INFO] resetting race"; int returnCode = 200; foreach(ScStwTimer *speedTimer, this->timers){ if(!speedTimer->reset() && speedTimer->getState() != ScStwTimer::DISABLED) { returnCode = 904; } } if(returnCode == 200){ this->setState(IDLE); } return returnCode; } void ScStwRace::cancelStart(bool falseStart){ // TODO: FIXME qDebug() << "+ [INFO] cancelling race: " << falseStart; if(falseStart){ // cancel all running timers foreach(ScStwTimer *timer, this->timers) { timer->cancel(); } this->setState(STOPPED); this->soundPlayer->cancel(this->soundVolume); } else { this->soundPlayer->cancel(this->soundVolume); this->nextActionTimer->stop(); this->nextActionLoop->quit(); this->nextStartAction = None; foreach(ScStwTimer *timer, this->timers){ timer->cancel(); } } } bool ScStwRace::playSoundsAndStartTimers(StartAction thisAction) { if(this->state != STARTING) return true; double timeOfSoundPlaybackStart; if(this->startActionSettings.contains(thisAction) && this->startActionSettings[thisAction]["Enabled"].toBool()) { if(!this->soundPlayer->play(thisAction, this->soundVolume, &timeOfSoundPlaybackStart)) return false; if(this->state != STARTING) return true; } if(thisAction < Start) { this->nextStartAction = StartAction(thisAction + 1); qDebug() << "next action is: " << nextStartAction; int nextActionDelay = -1; if(this->startActionSettings.contains(this->nextStartAction) && this->startActionSettings[this->nextStartAction]["Enabled"].toBool()) { nextActionDelay = this->startActionSettings[this->nextStartAction]["Delay"].toInt(); } // perform next action this->nextActionTimer->start(nextActionDelay > 0 ? nextActionDelay:1); if(nextActionDelay > 0) emit this->nextStartActionChanged(); this->nextActionLoop->exec(); return this->playSoundsAndStartTimers(this->nextStartAction); } // perform start // start all timers bool startOk = true; foreach(ScStwTimer *timer, this->timers){ if(!timer->start(timeOfSoundPlaybackStart + 3100) && timer->getState() != ScStwTimer::DISABLED){ startOk = false; } } if(!startOk) { qDebug() << "[ERROR][START] error staring all timers"; return false; } if(!this->soundPlayer->waitForSoundFinish()) { qDebug() << "[ERROR][START] start sound wait error"; return false; } // check if a false start occured if(this->state != STARTING) return true; nextStartAction = None; emit this->nextStartActionChanged(); this->setState(RUNNING); return true; } /** * @brief ScStwRace::setState * @param {QString} newState */ void ScStwRace::setState(RaceState newState) { if(newState != this->state) { qDebug() << "+ [INFO][MAIN] state changed: " << newState; this->state = newState; emit this->stateChanged(newState); if(this->state == IDLE) { // if we changed to IDLE -> enable timers foreach(ScStwTimer* timer, this->timerEnableQueque) { this->handleTimerEnable(timer); } this->timerEnableQueque.clear(); } } } /** * @brief ScStwRace::refreshTimerStates */ void ScStwRace::refreshTimerStates() { qDebug() << "[INFO][MAIN] refreshing timer states"; // check if the race is over bool raceIsOver = true; foreach(ScStwTimer * timer, this->timers){ if(timer->getState() < ScStwTimer::WON && timer->getState() != ScStwTimer::WAITING){ // if the timer is not in stoped state raceIsOver = false; break; } else if(timer->getState() == ScStwTimer::WAITING) { this->handleTimerStop(); } } if(raceIsOver){ this->setState(STOPPED); return; } } // ------------------------ // --- helper functions --- // ------------------------ /** * @brief ScStwRace::handleTimerEnable function to enable timers at the right moment to prevent them from bricking the state machine * @param {ScStwExtensionControlledTimer*} timer timer to be enabled */ void ScStwRace::handleTimerEnable(ScStwTimer* timer) { if(this->state == IDLE) { if(timer->getState() == ScStwTimer::DISABLED) { timer->setDisabled(false); } else { timer->setDisabled(true); } } else { this->timerEnableQueque.append(timer); } } QVariantList ScStwRace::getNextStartActionDetails() { int nextActionDelay = 0; double nextActionDelayProg = -1; if(this->nextStartAction == AtYourMarks || this->nextStartAction == Ready) { // get the total delay and the delay progress of the next action timer double remaining = this->nextActionTimer->remainingTime(); nextActionDelay = this->startActionSettings[this->nextStartAction]["Delay"].toInt(); if(remaining < 0) { remaining = nextActionDelay; } nextActionDelayProg = 1 - (remaining / nextActionDelay); } return { this->nextStartAction, nextActionDelay, nextActionDelayProg }; } bool ScStwRace::writeStartActionSetting(StartAction action, bool enabled, int delay) { if(action != AtYourMarks && action != Ready) return false; QVariantMap setting = {{"Enabled", enabled}, {"Delay", delay}}; if(!this->startActionSettings.contains(action)) this->startActionSettings.insert(action, setting); else this->startActionSettings[action] = setting; return true; } bool ScStwRace::setSoundVolume(double volume) { if(volume >= 0 && volume <= 1) { this->soundVolume = volume; return true; } else { return false; } } bool ScStwRace::addTimer(ScStwTimer *timer) { if(this->state != IDLE) return false; foreach(ScStwTimer *existingTimer, this->timers) { if(existingTimer == timer) return true; } this->timers.append(timer); connect(timer, &ScStwTimer::startCanceled, this, &ScStwRace::cancelStart); connect(timer, &ScStwTimer::stateChanged, this, &ScStwRace::refreshTimerStates); connect(timer, &ScStwTimer::requestEnableChange, this, &ScStwRace::handleTimerEnable); connect(timer, &ScStwTimer::stateChanged, this, &ScStwRace::timersChanged); connect(timer, &ScStwTimer::reactionTimeChanged, this, &ScStwRace::timersChanged); return true; } ScStwRace::RaceState ScStwRace::getState() { return this->state; } ScStwRace::StartAction ScStwRace::getNextStartAction() { return this->nextStartAction; } QVariantList ScStwRace::getTimerDetailList() { QVariantList tmpTimers; foreach(ScStwTimer * timer, this->timers){ QVariantMap tmpTimer; tmpTimer.insert("id", this->timers.indexOf(timer)); tmpTimer.insert("state", timer->getState()); tmpTimer.insert("currentTime", timer->getCurrentTime()); tmpTimer.insert("reactionTime", timer->getReactionTime()); tmpTimer.insert("text", timer->getText()); tmpTimers.append(tmpTimer); } return tmpTimers; }