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 <malin.ejdbo@gmail.com>
This commit is contained in:
Emma Broman
2021-08-16 09:29:45 +02:00
committed by GitHub
parent afe1f960cd
commit 8d7d8b9ba4
15 changed files with 1149 additions and 39 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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}
)

View File

@@ -0,0 +1 @@
set(DEFAULT_MODULE ON)

View File

@@ -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 <ghoul/misc/dictionary.h>
#include <string>
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__

View File

@@ -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 <modules/statemachine/include/state.h>
#include <modules/statemachine/include/transition.h>
#include <vector>
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<std::string> 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<State> _states;
std::vector<Transition> _transitions;
};
} // namespace openspace
#endif __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINE___H__

View File

@@ -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 <ghoul/misc/dictionary.h>
#include <string>
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__

View File

@@ -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 <modules/statemachine/include/state.h>
#include <openspace/documentation/documentation.h>
#include <openspace/engine/globals.h>
#include <openspace/scripting/scriptengine.h>
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<Parameters>("statemachine_state");
}
State::State(const ghoul::Dictionary& dictionary) {
const Parameters p = codegen::bake<Parameters>(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

View File

@@ -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 <modules/statemachine/include/statemachine.h>
#include <openspace/documentation/documentation.h>
#include <ghoul/logging/logmanager.h>
#include <optional>
namespace {
constexpr const char* _loggerCat = "StateMachine";
struct [[codegen::Dictionary(StateMachine)]] Parameters {
// A list of states
std::vector<ghoul::Dictionary> states
[[codegen::reference("statemachine_state")]];
// A list of transitions between the different states
std::vector<ghoul::Dictionary> transitions
[[codegen::reference("statemachine_transition")]];
// The initial state of the state machine. Defaults to the first in the list
std::optional<std::string> startState;
};
#include "statemachine_codegen.cpp"
} // namespace
namespace openspace {
documentation::Documentation StateMachine::Documentation() {
return codegen::doc<Parameters>("statemachine_statemachine");
}
StateMachine::StateMachine(const ghoul::Dictionary& dictionary) {
const Parameters p = codegen::bake<Parameters>(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<int>(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<int>(i);
}
}
return -1;
}
std::vector<std::string> StateMachine::possibleTransitions() const {
std::vector<std::string> 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

View File

@@ -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 <modules/statemachine/include/transition.h>
#include <openspace/documentation/documentation.h>
#include <openspace/engine/globals.h>
#include <openspace/scripting/scriptengine.h>
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<std::string> action;
};
#include "transition_codegen.cpp"
} // namespace
namespace openspace {
documentation::Documentation Transition::Documentation() {
return codegen::doc<Parameters>("statemachine_transition");
}
Transition::Transition(const ghoul::Dictionary& dictionary) {
const Parameters p = codegen::bake<Parameters>(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

View File

@@ -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 <modules/statemachine/statemachinemodule.h>
#include <modules/statemachine/include/state.h>
#include <modules/statemachine/include/statemachine.h>
#include <modules/statemachine/include/transition.h>
#include <openspace/documentation/documentation.h>
#include <openspace/scripting/lualibrary.h>
#include <ghoul/logging/logmanager.h>
#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<std::string> 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<StateMachine>(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<std::string> StateMachineModule::possibleTransitions() const {
if (!_machine) {
LWARNING("Attempting to use uninitialized state machine");
return std::vector<std::string>();
}
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<documentation::Documentation> StateMachineModule::documentations() const {
return {
State::Documentation(),
StateMachine::Documentation(),
Transition::Documentation()
};
}
} // namespace openspace

View File

@@ -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 <openspace/util/openspacemodule.h>
#include <modules/statemachine/include/statemachine.h>
#include <optional>
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<std::string> startState = std::nullopt);
bool hasStateMachine() const;
// initializeStateMachine must have been called before
void setInitialState(const std::string initialState);
std::string currentState() const;
std::vector<std::string> possibleTransitions() const;
void transitionTo(const std::string& newState);
bool canGoToState(const std::string& state) const;
scripting::LuaLibrary luaLibrary() const override;
std::vector<documentation::Documentation> documentations() const override;
private:
std::unique_ptr<StateMachine> _machine = nullptr;
};
} // namespace openspace
#endif __OPENSPACE_MODULE_STATEMACHINE___STATEMACHINEMODULE___H__

View File

@@ -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 <modules/statemachine/statemachinemodule.h>
#include <openspace/engine/globals.h>
#include <openspace/engine/moduleengine.h>
#include <openspace/scripting/scriptengine.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/misc.h>
#include <optional>
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<std::string> startState = std::nullopt;
if (nArguments > 2) {
startState = ghoul::lua::value<std::string>(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<StateMachineModule>();
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<StateMachineModule>();
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<StateMachineModule>();
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<StateMachineModule>();
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<StateMachineModule>();
std::vector<std::string> 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<StateMachineModule>();
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<StateMachineModule>();
if (module->hasStateMachine()) {
std::string currentState = module->currentState();
std::vector<std::string> 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