From 8d7d8b9ba4aa275dcce5c8eb241d34d57fcd8a51 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Mon, 16 Aug 2021 09:29:45 +0200 Subject: [PATCH] Feature/state machine (#1705) * Fix script error in old state machine example * Add a module with a more complex state machine, that can be created and controlled through the Lua API. Useful for interactive installations * Add an example asset for the new state machine and rename the old linear "state machine" to luastatemachine Co-authored-by: Malin Ejdbo --- data/assets/examples/luastatemachine.asset | 47 +++++ data/assets/examples/statemachine.asset | 104 +++++---- ...r.asset => lua_state_machine_helper.asset} | 5 + ext/ghoul | 2 +- modules/statemachine/CMakeLists.txt | 47 +++++ modules/statemachine/include.cmake | 1 + modules/statemachine/include/state.h | 55 +++++ modules/statemachine/include/statemachine.h | 64 ++++++ modules/statemachine/include/transition.h | 54 +++++ modules/statemachine/src/state.cpp | 80 +++++++ modules/statemachine/src/statemachine.cpp | 198 ++++++++++++++++++ modules/statemachine/src/transition.cpp | 78 +++++++ modules/statemachine/statemachinemodule.cpp | 193 +++++++++++++++++ modules/statemachine/statemachinemodule.h | 65 ++++++ .../statemachine/statemachinemodule_lua.inl | 195 +++++++++++++++++ 15 files changed, 1149 insertions(+), 39 deletions(-) create mode 100644 data/assets/examples/luastatemachine.asset rename data/assets/util/{state_machine_helper.asset => lua_state_machine_helper.asset} (87%) create mode 100644 modules/statemachine/CMakeLists.txt create mode 100644 modules/statemachine/include.cmake create mode 100644 modules/statemachine/include/state.h create mode 100644 modules/statemachine/include/statemachine.h create mode 100644 modules/statemachine/include/transition.h create mode 100644 modules/statemachine/src/state.cpp create mode 100644 modules/statemachine/src/statemachine.cpp create mode 100644 modules/statemachine/src/transition.cpp create mode 100644 modules/statemachine/statemachinemodule.cpp create mode 100644 modules/statemachine/statemachinemodule.h create mode 100644 modules/statemachine/statemachinemodule_lua.inl diff --git a/data/assets/examples/luastatemachine.asset b/data/assets/examples/luastatemachine.asset new file mode 100644 index 0000000000..45ce5bb29a --- /dev/null +++ b/data/assets/examples/luastatemachine.asset @@ -0,0 +1,47 @@ +local stateMachineHelper = asset.require('util/lua_state_machine_helper') + +local states = { + { + Title = "Highlight EarthTrail", + Play = function () + openspace.setPropertyValue("Scene.EarthTrail.Renderable.Appearance.LineWidth", 10, 1) + end, + Rewind = function () + openspace.setPropertyValue("Scene.EarthTrail.Renderable.Appearance.LineWidth", 2, 1) + end + }, + { + Title = "Highlight MarsTrail", + Play = function () + openspace.setPropertyValue("Scene.EarthTrail.Renderable.Appearance.LineWidth", 2, 1) + openspace.setPropertyValue("Scene.MarsTrail.Renderable.Appearance.LineWidth", 10, 1) + end, + Rewind = function () + openspace.setPropertyValue("Scene.MarsTrail.Renderable.Appearance.LineWidth", 2, 1) + openspace.setPropertyValue("Scene.EarthTrail.Renderable.Appearance.LineWidth", 10, 1) + end + } +} + +local stateMachine + +function next() + stateMachine.goToNextState() +end + +function previous() + stateMachine.goToPreviousState() +end + +asset.onInitialize(function () + stateMachine = stateMachineHelper.createStateMachine(states) + openspace.bindKey('RIGHT', 'next()') + openspace.bindKey('LEFT', 'previous()') +end) + + +asset.onDeinitialize(function () + stateMachine = nil + openspace.clearKey('RIGHT') + openspace.clearKey('LEFT') +end) diff --git a/data/assets/examples/statemachine.asset b/data/assets/examples/statemachine.asset index e97213011c..fd54c11bea 100644 --- a/data/assets/examples/statemachine.asset +++ b/data/assets/examples/statemachine.asset @@ -1,47 +1,75 @@ -local stateMachineHelper = asset.require('util/state_machine_helper') +-- Create a state machine with a few different states. The state machine can be controlled through +-- the scripting commands from the state machine module. -states = { +local targetNode = function(nodeIdentifier) + return [[ + openspace.setPropertyValueSingle("NavigationHandler.OrbitalNavigator.RetargetAnchor", nil) + openspace.setPropertyValueSingle( + "NavigationHandler.OrbitalNavigator.Anchor", + ']] .. nodeIdentifier .. [[' + ) + openspace.setPropertyValueSingle("NavigationHandler.OrbitalNavigator.Aim", '') + ]] +end + +local states = { { - Title = "Highlight EarthTrail", - Play = function () - openspace.setPropertyValue("Scene.EarthTrail.Renderable.LineWidth", 10, 1) - end, - Rewind = function () - openspace.setPropertyValue("Scene.EarthTrail.Renderable.LineWidth", 2, 1) - end - }, + Identifier = "Constellations", + Enter = [[ + openspace.setPropertyValueSingle('Scene.Constellations.Renderable.Opacity', 1.0, 1.0) + ]], + Exit = [[ + openspace.setPropertyValueSingle('Scene.Constellations.Renderable.Opacity', 0.0, 1.0) + ]] + }, { - Title = "Highlight MarsTrail", - Play = function () - openspace.setPropertyValue("Scene.EarthTrail.Renderable.LineWidth", 2, 1) - openspace.setPropertyValue("Scene.MarsTrail.Renderable.LineWidth", 10, 1) - end, - Rewind = function () - openspace.setPropertyValue("Scene.MarsTrail.Renderable.LineWidth", 2, 1) - openspace.setPropertyValue("Scene.EarthTrail.Renderable.LineWidth", 10, 1) - end + Identifier = "Earth", + Enter = "openspace.setPropertyValueSingle('Scene.EarthLabel.Renderable.Enabled', true)", + Exit = "openspace.setPropertyValueSingle('Scene.EarthLabel.Renderable.Enabled', false)" + }, + { + Identifier = "Moon", + Enter = "", + Exit = "" } } -local stateMachine +local transitions = { + { + From = "Earth", + To = "Moon", + Action = targetNode("Moon") + }, + { + From = "Moon", + To = "Earth", + Action = targetNode("Earth") + }, + { + From = "Earth", + To = "Constellations", + -- action is optional + }, + { + From = "Constellations", + To = "Earth" + }, + { + From = "Moon", + To = "Constellations", + Action = targetNode("Earth") + }, + { + From = "Constellations", + To = "Moon", + Action = targetNode("Moon") + } +} -function next() - stateMachine.goToNextState() -end +asset.onInitialize(function() + -- Setup + openspace.setPropertyValueSingle('Scene.Constellations.Renderable.Enabled', true) + openspace.setPropertyValueSingle('Scene.Constellations.Renderable.Opacity', 0.0) -function previous() - stateMachine.goToPreviousState() -end - -asset.onInitialize(function () - stateMachine = stateMachineHelper.createStateMachine(states) - openspace.bindKey('RIGHT', 'next()') - openspace.bindKey('LEFT', 'previous()') -end) - - -asset.onDeinitialize(function () - stateMachine = nil - openspace.clearKey('RIGHT') - openspace.clearKey('LEFT') + openspace.statemachine.createStateMachine(states, transitions, "Earth") end) diff --git a/data/assets/util/state_machine_helper.asset b/data/assets/util/lua_state_machine_helper.asset similarity index 87% rename from data/assets/util/state_machine_helper.asset rename to data/assets/util/lua_state_machine_helper.asset index 30f0f2ff3c..45369d01fb 100644 --- a/data/assets/util/state_machine_helper.asset +++ b/data/assets/util/lua_state_machine_helper.asset @@ -1,3 +1,8 @@ +-- Contains the required functions to create a simple Lua state machine, that can step +-- forwards and backwards through a list of states. +-- +-- A state is given as a table with a Title string, and two functions: Play and Rewind +-- (see example asset) local goToNextStateFunction = function (machine) if (machine.currentStateIndex >= #machine.states) then diff --git a/ext/ghoul b/ext/ghoul index 1febcfdee2..bce78a781c 160000 --- a/ext/ghoul +++ b/ext/ghoul @@ -1 +1 @@ -Subproject commit 1febcfdee2bf453e9485d72092d0ae1c0db2b1b0 +Subproject commit bce78a781c0da924e54af01e031ecc3de62441f5 diff --git a/modules/statemachine/CMakeLists.txt b/modules/statemachine/CMakeLists.txt new file mode 100644 index 0000000000..cd4f550004 --- /dev/null +++ b/modules/statemachine/CMakeLists.txt @@ -0,0 +1,47 @@ +########################################################################################## +# # +# OpenSpace # +# # +# Copyright (c) 2014-2021 # +# # +# 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(${OPENSPACE_CMAKE_EXT_DIR}/module_definition.cmake) + +set(HEADER_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/include/state.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/transition.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/statemachine.h +) +source_group("Header Files" FILES ${HEADER_FILES}) + +set(SOURCE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/statemachinemodule_lua.inl + ${CMAKE_CURRENT_SOURCE_DIR}/src/state.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/transition.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/statemachine.cpp +) +source_group("Source Files" FILES ${SOURCE_FILES}) + +create_new_module( + "StateMachine" + statemachine_module + STATIC + ${HEADER_FILES} ${SOURCE_FILES} +) diff --git a/modules/statemachine/include.cmake b/modules/statemachine/include.cmake new file mode 100644 index 0000000000..ffea0ac430 --- /dev/null +++ b/modules/statemachine/include.cmake @@ -0,0 +1 @@ +set(DEFAULT_MODULE ON) diff --git a/modules/statemachine/include/state.h b/modules/statemachine/include/state.h new file mode 100644 index 0000000000..9790697830 --- /dev/null +++ b/modules/statemachine/include/state.h @@ -0,0 +1,55 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_STATEMACHINE___STATE___H__ +#define __OPENSPACE_MODULE_STATEMACHINE___STATE___H__ + +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class State { +public: + explicit State(const ghoul::Dictionary& dictionary); + ~State() = default; + + void enter() const; + void exit() const; + + std::string name() const; + + static documentation::Documentation Documentation(); + +private: + std::string _name; + std::string _enter; + std::string _exit; +}; + +} // namespace openspace + +#endif __OPENSPACE_MODULE_STATEMACHINE___STATE___H__ diff --git a/modules/statemachine/include/statemachine.h b/modules/statemachine/include/statemachine.h new file mode 100644 index 0000000000..1bb3811a41 --- /dev/null +++ b/modules/statemachine/include/statemachine.h @@ -0,0 +1,64 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINE___H__ +#define __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINE___H__ + +#include +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class StateMachine { +public: + explicit StateMachine(const ghoul::Dictionary& dictionary); + ~StateMachine() = default; + + void setInitialState(const std::string initialState); + const State* currentState() const; + void transitionTo(const std::string& newState); + bool canTransitionTo(const std::string& state) const; + + /* + * Return the identifiers of all possible transitions from the current state + */ + std::vector possibleTransitions() const; + + static documentation::Documentation Documentation(); + +private: + int findTransitionTo(const std::string& state) const; + int findState(const std::string& state) const; + + int _currentStateIndex = -1; + std::vector _states; + std::vector _transitions; +}; + +} // namespace openspace + +#endif __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINE___H__ diff --git a/modules/statemachine/include/transition.h b/modules/statemachine/include/transition.h new file mode 100644 index 0000000000..7dd5932386 --- /dev/null +++ b/modules/statemachine/include/transition.h @@ -0,0 +1,54 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_STATEMACHINE___TRANSITION___H__ +#define __OPENSPACE_MODULE_STATEMACHINE___TRANSITION___H__ + +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class Transition { +public: + explicit Transition(const ghoul::Dictionary& dictionary); + ~Transition() = default; + + const std::string& from() const; + const std::string& to() const; + void performAction() const; + + static documentation::Documentation Documentation(); + +private: + std::string _from; + std::string _to; + std::string _action; +}; + +} // namespace openspace + +#endif __OPENSPACE_MODULE_STATEMACHINE___TRANSITION___H__ diff --git a/modules/statemachine/src/state.cpp b/modules/statemachine/src/state.cpp new file mode 100644 index 0000000000..bc67dc9f68 --- /dev/null +++ b/modules/statemachine/src/state.cpp @@ -0,0 +1,80 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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 + +namespace { + struct [[codegen::Dictionary(State)]] Parameters { + // A string that will be used to identify the state. Cannot be the same as + // any other state in the machine + std::string identifier; + + // A string containing a Lua script that will be executed when the state + // is entered, i.e on a transition from another state + std::string enter; + + // A string containing a Lua script that will be executed when the state + // is exited, i.e on a transition to another state + std::string exit; + }; +#include "state_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation State::Documentation() { + return codegen::doc("statemachine_state"); +} + +State::State(const ghoul::Dictionary& dictionary) { + const Parameters p = codegen::bake(dictionary); + + _name = p.identifier; + _enter = p.enter; + _exit = p.exit; +} + +void State::enter() const { + global::scriptEngine->queueScript( + _enter, + scripting::ScriptEngine::RemoteScripting::Yes + ); +} + +void State::exit() const { + global::scriptEngine->queueScript( + _exit, + scripting::ScriptEngine::RemoteScripting::Yes + ); +} + +std::string State::name() const { + return _name; +} + +} // namespace openspace diff --git a/modules/statemachine/src/statemachine.cpp b/modules/statemachine/src/statemachine.cpp new file mode 100644 index 0000000000..aa160c302f --- /dev/null +++ b/modules/statemachine/src/statemachine.cpp @@ -0,0 +1,198 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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 + +namespace { + constexpr const char* _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; +} + +} // namespace openspace diff --git a/modules/statemachine/src/transition.cpp b/modules/statemachine/src/transition.cpp new file mode 100644 index 0000000000..c793ca6da7 --- /dev/null +++ b/modules/statemachine/src/transition.cpp @@ -0,0 +1,78 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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 + +namespace { + struct [[codegen::Dictionary(Transition)]] Parameters { + // The identifier of the state that can trigger the transition + std::string from; + + // The identifier of the state that the state machine will move to after the + // transition + std::string to; + + // A string containing a Lua script that will be executed when the transition + // is triggered + std::optional action; + }; +#include "transition_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation Transition::Documentation() { + return codegen::doc("statemachine_transition"); +} + +Transition::Transition(const ghoul::Dictionary& dictionary) { + const Parameters p = codegen::bake(dictionary); + _from = p.from; + _to = p.to; + _action = p.action.value_or(""); +} + +const std::string& Transition::from() const { + return _from; +} + +const std::string& Transition::to() const { + return _to; +} + +void Transition::performAction() const { + if (_action.empty()) { + return; + } + global::scriptEngine->queueScript( + _action, + scripting::ScriptEngine::RemoteScripting::Yes + ); +} + +} // namespace openspace diff --git a/modules/statemachine/statemachinemodule.cpp b/modules/statemachine/statemachinemodule.cpp new file mode 100644 index 0000000000..9b4f5e1a34 --- /dev/null +++ b/modules/statemachine/statemachinemodule.cpp @@ -0,0 +1,193 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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 +#include + +#include "statemachinemodule_lua.inl" + +namespace { + constexpr const char* _loggerCat = "StateMachine"; +} // namespace + +namespace openspace { + +StateMachineModule::StateMachineModule() + : OpenSpaceModule(Name) +{ } + +void StateMachineModule::initializeStateMachine(const ghoul::Dictionary& states, + const ghoul::Dictionary& transitions, + const std::optional startState) +{ + ghoul::Dictionary dictionary; + dictionary.setValue("States", states); + dictionary.setValue("Transitions", transitions); + + if (startState.has_value()) { + dictionary.setValue("StartState", *startState); + } + + try { + _machine = std::make_unique(dictionary); + LINFO(fmt::format( + "State machine was created with start state: {}", currentState() + )); + } + catch (const documentation::SpecificationError& e) { + LERROR(ghoul::to_string(e.result)); + LERROR(fmt::format("Error loading state machine: {}", e.what())); + } +} + +bool StateMachineModule::hasStateMachine() const { + return _machine != nullptr; +} + +void StateMachineModule::setInitialState(const std::string initialState) { + if (!_machine) { + LWARNING("Attempting to use uninitialized state machine"); + return; + } + + _machine->setInitialState(initialState); +} + +std::string StateMachineModule::currentState() const { + if (!_machine || !_machine->currentState()) { + LWARNING("Attempting to use uninitialized state machine"); + return ""; + } + + return _machine->currentState()->name(); +} + +std::vector StateMachineModule::possibleTransitions() const { + if (!_machine) { + LWARNING("Attempting to use uninitialized state machine"); + return std::vector(); + } + + return _machine->possibleTransitions(); +} + +void StateMachineModule::transitionTo(const std::string& newState) { + if (!_machine) { + LWARNING("Attempting to use uninitialized state machine"); + return; + } + + _machine->transitionTo(newState); +} + +bool StateMachineModule::canGoToState(const std::string& state) const { + if (!_machine) { + LWARNING("Attempting to use uninitialized state machine"); + return false; + } + + return _machine->canTransitionTo(state); +} + +scripting::LuaLibrary StateMachineModule::luaLibrary() const { + scripting::LuaLibrary res; + res.name = "statemachine"; + res.functions = { + { + "createStateMachine", + &luascriptfunctions::createStateMachine, + {}, + "table, table, [string]", + "Creates a state machine from a list of states and transitions. See State " + "and Transition documentation for details. The optional thrid argument is " + "the identifier of the desired initial state. If left out, the first state " + "in the list will be used." + }, + { + "goToState", + &luascriptfunctions::goToState, + {}, + "string", + "Triggers a transition from the current state to the state with the given " + "identifier. Requires that the specified string corresponds to an existing " + "state, and that a transition between the two states exists." + }, + { + "setInitialState", + &luascriptfunctions::setInitialState, + {}, + "string", + "Immediately sets the current state to the state with the given name, if " + "it exists. This is done without doing a transition and completely ignores " + "the previous state." + }, + { + "currentState", + &luascriptfunctions::currentState, + {}, + "", + "Returns the string name of the current state that the statemachine is in." + }, + { + "possibleTransitions", + &luascriptfunctions::possibleTransitions, + {}, + "", + "Returns a list with the identifiers of all the states that can be " + "transitioned to from the current state." + }, + { + "canGoToState", + &luascriptfunctions::canGoToState, + {}, + "string", + "Returns true if there is a defined transition between the current state and " + "the given string name of a state, otherwise false" + }, + { + "printCurrentStateInfo", + &luascriptfunctions::printCurrentStateInfo, + {}, + "", + "Prints information about the current state and possible transitions to the log." + } + }; + return res; +} + +std::vector StateMachineModule::documentations() const { + return { + State::Documentation(), + StateMachine::Documentation(), + Transition::Documentation() + }; +} + +} // namespace openspace diff --git a/modules/statemachine/statemachinemodule.h b/modules/statemachine/statemachinemodule.h new file mode 100644 index 0000000000..49ded93911 --- /dev/null +++ b/modules/statemachine/statemachinemodule.h @@ -0,0 +1,65 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINEMODULE___H__ +#define __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINEMODULE___H__ + +#include + +#include +#include + +namespace openspace { + +class StateMachineModule : public OpenSpaceModule { +public: + constexpr static const char* Name = "StateMachine"; + + StateMachineModule(); + ~StateMachineModule() = default; + + void initializeStateMachine(const ghoul::Dictionary& states, + const ghoul::Dictionary& transitions, + const std::optional startState = std::nullopt); + + bool hasStateMachine() const; + + // initializeStateMachine must have been called before + void setInitialState(const std::string initialState); + std::string currentState() const; + std::vector possibleTransitions() const; + void transitionTo(const std::string& newState); + bool canGoToState(const std::string& state) const; + + scripting::LuaLibrary luaLibrary() const override; + + std::vector documentations() const override; + +private: + std::unique_ptr _machine = nullptr; +}; + +} // namespace openspace + +#endif __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINEMODULE___H__ diff --git a/modules/statemachine/statemachinemodule_lua.inl b/modules/statemachine/statemachinemodule_lua.inl new file mode 100644 index 0000000000..0cacd48dc9 --- /dev/null +++ b/modules/statemachine/statemachinemodule_lua.inl @@ -0,0 +1,195 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2021 * + * * + * 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 +#include + +namespace openspace::luascriptfunctions { + +int createStateMachine(lua_State* L) { + const int nArguments = ghoul::lua::checkArgumentsAndThrow( + L, + { 2, 3 }, + "lua::createStateMachine" + ); + + // If three arguments, a start state was included + std::optional startState = std::nullopt; + if (nArguments > 2) { + startState = ghoul::lua::value(L, 3, ghoul::lua::PopValue::Yes); + } + + // Last dictionary is on top of the stack + ghoul::Dictionary transitions; + try { + ghoul::lua::luaDictionaryFromState(L, transitions); + } + catch (const ghoul::lua::LuaFormatException& e) { + LERRORC("createStateMachine", e.what()); + return 0; + } + + // Pop, so that first dictionary is on top and can be read + lua_pop(L, 1); + ghoul::Dictionary states; + try { + ghoul::lua::luaDictionaryFromState(L, states); + } + catch (const ghoul::lua::LuaFormatException& e) { + LERRORC("createStateMachine", e.what()); + return 0; + } + + StateMachineModule* module = global::moduleEngine->module(); + + module->initializeStateMachine(states, transitions, startState); + + lua_settop(L, 0); + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int goToState(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::goToState"); + const bool isString = (lua_isstring(L, 1) != 0); + + if (!isString) { + lua_settop(L, 0); + const char* msg = lua_pushfstring( + L, + "%s expected, got %s", + lua_typename(L, LUA_TSTRING), + luaL_typename(L, 0) + ); + return luaL_error(L, "bad argument #%d (%s)", 1, msg); + } + + const std::string newState = lua_tostring(L, 1); + StateMachineModule* module = global::moduleEngine->module(); + module->transitionTo(newState); + LINFOC("StateMachine", "Transitioning to " + newState); + + lua_settop(L, 0); + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int setInitialState(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::setStartState"); + const bool isString = (lua_isstring(L, 1) != 0); + + if (!isString) { + lua_settop(L, 0); + const char* msg = lua_pushfstring( + L, + "%s expected, got %s", + lua_typename(L, LUA_TSTRING), + luaL_typename(L, 0) + ); + return luaL_error(L, "bad argument #%d (%s)", 1, msg); + } + + const std::string startState = lua_tostring(L, 1); + StateMachineModule* module = global::moduleEngine->module(); + module->setInitialState(startState); + LINFOC("StateMachine", "Initial state set to: " + startState); + + lua_settop(L, 0); + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 0; +} + +int currentState(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::currentState"); + + StateMachineModule* module = global::moduleEngine->module(); + std::string currentState = module->currentState(); + + lua_pushstring(L, currentState.c_str()); + ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack"); + return 1; +} + +int possibleTransitions(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::possibleTransitions"); + + StateMachineModule* module = global::moduleEngine->module(); + std::vector transitions = module->possibleTransitions(); + + ghoul::lua::push(L, transitions); + ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack"); + return 1; +} + +int canGoToState(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::canGoToState"); + const bool isString = (lua_isstring(L, 1) != 0); + + if (!isString) { + lua_settop(L, 0); + const char* msg = lua_pushfstring( + L, + "%s expected, got %s", + lua_typename(L, LUA_TSTRING), + luaL_typename(L, 0) + ); + return luaL_error(L, "bad argument #%d (%s)", 1, msg); + } + + const std::string state = lua_tostring(L, 1); + StateMachineModule* module = global::moduleEngine->module(); + ghoul::lua::push(L, module->canGoToState(state)); + + ghoul_assert(lua_gettop(L) == 1, "Incorrect number of items left on stack"); + return 1; +} + +int printCurrentStateInfo(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, 0, "lua::printCurrentStateInfo"); + + StateMachineModule* module = global::moduleEngine->module(); + + if (module->hasStateMachine()) { + std::string currentState = module->currentState(); + std::vector transitions = module->possibleTransitions(); + LINFOC("StateMachine", fmt::format( + "Currently in state: '{}'. Can transition to states: [ {} ]", + currentState, + ghoul::join(transitions, ",") + )); + } + else { + LINFOC("StateMachine", "No state machine has been created"); + } + + ghoul_assert(lua_gettop(L) == 0, "Incorrect number of items left on stack"); + return 1; +} + +} //namespace openspace::luascriptfunctions