/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2023 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include namespace { constexpr std::string_view _loggerCat = "StateMachine"; struct [[codegen::Dictionary(StateMachine)]] Parameters { // A list of states std::vector states [[codegen::reference("statemachine_state")]]; // A list of transitions between the different states std::vector transitions [[codegen::reference("statemachine_transition")]]; // The initial state of the state machine. Defaults to the first in the list std::optional startState; }; #include "statemachine_codegen.cpp" } // namespace namespace openspace { documentation::Documentation StateMachine::Documentation() { return codegen::doc("statemachine_statemachine"); } StateMachine::StateMachine(const ghoul::Dictionary& dictionary) { const Parameters p = codegen::bake(dictionary); _states.reserve(p.states.size()); for (const ghoul::Dictionary& s : p.states) { _states.push_back(State(s)); } _transitions.reserve(p.transitions.size()); for (const ghoul::Dictionary& t : p.transitions) { const Transition trans = Transition(t); // Check so transition has valid identifiers bool foundFrom = findState(trans.from()) != -1; bool foundTo = findState(trans.to()) != -1; if (foundFrom && foundTo) { _transitions.push_back(trans); } else { LERROR(fmt::format( "Invalid transition from '{}' to '{}'. One or both of the states do not " "exist in the state machine", trans.from(), trans.to() )); } } _transitions.shrink_to_fit(); if (_transitions.empty()) { LWARNING("Created state machine without transitions"); } if (_states.empty()) { LERROR("Created state machine with no states"); return; } const std::string startState = p.startState.value_or(_states.front().name()); setInitialState(startState); } void StateMachine::setInitialState(const std::string initialState) { int stateIndex = findState(initialState); if (stateIndex == -1) { LWARNING(fmt::format( "Attempting to initialize with undefined state '{}'", initialState )); return; } _currentStateIndex = stateIndex; currentState()->enter(); } const State* StateMachine::currentState() const { if (_currentStateIndex == -1) { return nullptr; } return &_states[_currentStateIndex]; } void StateMachine::transitionTo(const std::string& newState) { if (!currentState()) { LERROR( "Cannot perform transition as the machine is in no current state. First set " "an initial state" ); return; } int stateIndex = findState(newState); if (stateIndex == -1) { LWARNING(fmt::format( "Attempting to transition to undefined state '{}'", newState )); return; } int transitionIndex = findTransitionTo(newState); if (transitionIndex == -1) { LWARNING(fmt::format( "Transition from '{}' to '{}' is undefined", currentState()->name(), newState )); return; } currentState()->exit(); _transitions[transitionIndex].performAction(); _currentStateIndex = stateIndex; currentState()->enter(); } bool StateMachine::canTransitionTo(const std::string& state) const { const int transitionIndex = findTransitionTo(state); return transitionIndex != -1; } // Search if the transition from _currentState to newState exists. // If yes then return the index to the transition, otherwise return -1 int StateMachine::findTransitionTo(const std::string& state) const { if (!currentState()) { return -1; } for (size_t i = 0; i < _transitions.size(); ++i) { if (_transitions[i].from() == currentState()->name() && _transitions[i].to() == state) { return static_cast(i); } } return -1; } // Search if the state exist. // If yes then return the index to the state, otherwise return -1 int StateMachine::findState(const std::string& state) const { for (size_t i = 0; i < _states.size(); ++i) { if (_states[i].name() == state) { return static_cast(i); } } return -1; } std::vector StateMachine::possibleTransitions() const { std::vector res; if (!currentState()) { return res; } res.reserve(_transitions.size()); for (size_t i = 0; i < _transitions.size(); ++i) { if (_transitions[i].from() == currentState()->name()) { res.push_back(_transitions[i].to()); } } return res; } void StateMachine::saveToDotFile(const std::string& filename) const { const std::string outputFile = filename + ".dot"; std::ofstream file(outputFile); if (!file.good()) { LERROR(fmt::format( "Error opening file {} for saving state machine dot file", outputFile )); return; } file << "digraph statemachine {" << std::endl; for (const State& s : _states) { file << "\t" << s.name() << ";" << std::endl; } for (const Transition& t : _transitions) { file << "\t" << t.from() << " -> " << t.to() << ";" << std::endl; } file << "}" << std::endl; LINFO(fmt::format("Saved state machine to file: {}", outputFile)); } } // namespace openspace