State-machine structure, initial work.
This commit is contained in:
parent
bd71635c49
commit
22ea8ebe25
@ -23,6 +23,7 @@ set(HEADERS
|
||||
exception.hxx
|
||||
intern.hxx
|
||||
subsystem_mgr.hxx
|
||||
StateMachine.hxx
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
@ -36,7 +37,15 @@ set(SOURCES
|
||||
commands.cxx
|
||||
event_mgr.cxx
|
||||
exception.cxx
|
||||
subsystem_mgr.cxx
|
||||
subsystem_mgr.cxx
|
||||
StateMachine.cxx
|
||||
)
|
||||
|
||||
simgear_component(structure structure "${SOURCES}" "${HEADERS}")
|
||||
|
||||
if(ENABLE_TESTS)
|
||||
add_executable(test_state_machine state_machine_test.cxx)
|
||||
target_link_libraries(test_state_machine SimGearCore)
|
||||
add_test(test_state_machine ${EXECUTABLE_OUTPUT_PATH}/test_state_machine)
|
||||
|
||||
endif(ENABLE_TESTS)
|
||||
|
@ -11,6 +11,7 @@
|
||||
# include <simgear_config.h>
|
||||
#endif
|
||||
|
||||
#include <boost/foreach.hpp>
|
||||
#include <simgear/compiler.h>
|
||||
#include "SGBinding.hxx"
|
||||
|
||||
@ -23,6 +24,14 @@ SGBinding::SGBinding()
|
||||
{
|
||||
}
|
||||
|
||||
SGBinding::SGBinding(const std::string& commandName)
|
||||
: _command(0),
|
||||
_arg(0),
|
||||
_setting(0)
|
||||
{
|
||||
_command_name = commandName;
|
||||
}
|
||||
|
||||
SGBinding::SGBinding(const SGPropertyNode* node, SGPropertyNode* root)
|
||||
: _command(0),
|
||||
_arg(0),
|
||||
@ -98,3 +107,10 @@ SGBinding::fire (double setting) const
|
||||
fire();
|
||||
}
|
||||
}
|
||||
|
||||
void fireBindingList(const SGBindingList& aBindings)
|
||||
{
|
||||
BOOST_FOREACH(SGBinding_ptr b, aBindings) {
|
||||
b->fire();
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,12 @@ public:
|
||||
*/
|
||||
SGBinding ();
|
||||
|
||||
/**
|
||||
* Convenience constructor.
|
||||
*
|
||||
* @param node The binding will be built from this node.
|
||||
*/
|
||||
SGBinding(const std::string& commandName);
|
||||
|
||||
/**
|
||||
* Convenience constructor.
|
||||
@ -119,7 +125,15 @@ private:
|
||||
mutable SGPropertyNode_ptr _setting;
|
||||
};
|
||||
|
||||
typedef std::vector<SGSharedPtr<SGBinding> > SGBindingList;
|
||||
typedef SGSharedPtr<SGBinding> SGBinding_ptr;
|
||||
|
||||
typedef std::vector<SGBinding_ptr > SGBindingList;
|
||||
typedef std::map<unsigned,SGBindingList> SGBindingMap;
|
||||
|
||||
/**
|
||||
* fire every binding in a list, in sequence
|
||||
|
||||
*/
|
||||
void fireBindingList(const SGBindingList& aBindings);
|
||||
|
||||
#endif
|
||||
|
456
simgear/structure/StateMachine.cxx
Normal file
456
simgear/structure/StateMachine.cxx
Normal file
@ -0,0 +1,456 @@
|
||||
/* -*-c++-*-
|
||||
*
|
||||
* Copyright (C) 2013 James Turner
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <simgear_config.h>
|
||||
#endif
|
||||
|
||||
#include "StateMachine.hxx"
|
||||
|
||||
#include <cassert>
|
||||
#include <set>
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
#include <simgear/debug/logstream.hxx>
|
||||
#include <simgear/structure/SGBinding.hxx>
|
||||
#include <simgear/props/condition.hxx>
|
||||
#include <simgear/timing/timestamp.hxx>
|
||||
#include <simgear/structure/exception.hxx>
|
||||
|
||||
namespace simgear
|
||||
{
|
||||
|
||||
typedef std::vector<StateMachine::State_ptr> StatePtrVec;
|
||||
|
||||
static void readBindingList(SGPropertyNode* desc, const std::string& name,
|
||||
SGPropertyNode* root, SGBindingList& result)
|
||||
{
|
||||
BOOST_FOREACH(SGPropertyNode* b, desc->getChildren(name)) {
|
||||
SGBinding* bind = new SGBinding;
|
||||
bind->read(b, root);
|
||||
result.push_back(bind);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class StateMachine::State::StatePrivate
|
||||
{
|
||||
public:
|
||||
std::string _name;
|
||||
SGBindingList _updateBindings,
|
||||
_entryBindings,
|
||||
_exitBindings;
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class StateMachine::Transition::TransitionPrivate
|
||||
{
|
||||
public:
|
||||
std::string _name;
|
||||
SGBindingList _bindings;
|
||||
std::set<State*> _sourceStates; ///< weak refs to source states
|
||||
State* _target;
|
||||
SGSharedPtr<SGCondition> _condition;
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class StateMachine::StateMachinePrivate : public SGPropertyChangeListener
|
||||
{
|
||||
public:
|
||||
StateMachinePrivate(StateMachine* p) : _p(p) { }
|
||||
|
||||
void computeEligibleTransitions()
|
||||
{
|
||||
_eligible.clear();
|
||||
BOOST_FOREACH(Transition_ptr t, _transitions) {
|
||||
if (t->applicableForState(_currentState)) {
|
||||
_eligible.push_back(t.ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateMachine* _p;
|
||||
State_ptr _currentState;
|
||||
StatePtrVec _states;
|
||||
std::vector<Transition_ptr> _transitions;
|
||||
std::vector<Transition*> _eligible;
|
||||
SGTimeStamp _timeInState;
|
||||
|
||||
bool _listenerLockout; ///< block our listener when self-updating props
|
||||
virtual void valueChanged(SGPropertyNode* changed)
|
||||
{
|
||||
if (_listenerLockout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changed == _currentStateIndex) {
|
||||
State_ptr s = _p->stateByIndex(changed->getIntValue());
|
||||
_p->changeToState(s);
|
||||
} else if (changed == _currentStateName) {
|
||||
_p->changeToStateName(changed->getStringValue());
|
||||
}
|
||||
}
|
||||
|
||||
// exposed properties
|
||||
SGPropertyNode_ptr _root;
|
||||
SGPropertyNode_ptr _currentStateIndex;
|
||||
SGPropertyNode_ptr _currentStateName;
|
||||
SGPropertyNode_ptr _timeInStateProp;
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
StateMachine::State::State(const std::string& aName) :
|
||||
d(new StatePrivate)
|
||||
{
|
||||
d->_name = aName;
|
||||
}
|
||||
|
||||
StateMachine::State::~State()
|
||||
{
|
||||
}
|
||||
|
||||
std::string StateMachine::State::name() const
|
||||
{
|
||||
return d->_name;
|
||||
}
|
||||
|
||||
void StateMachine::State::update()
|
||||
{
|
||||
fireBindingList(d->_updateBindings);
|
||||
}
|
||||
|
||||
void StateMachine::State::fireEntryBindings()
|
||||
{
|
||||
fireBindingList(d->_entryBindings);
|
||||
}
|
||||
|
||||
void StateMachine::State::fireExitBindings()
|
||||
{
|
||||
fireBindingList(d->_exitBindings);
|
||||
}
|
||||
|
||||
void StateMachine::State::addUpdateBinding(SGBinding* aBinding)
|
||||
{
|
||||
d->_updateBindings.push_back(aBinding);
|
||||
}
|
||||
|
||||
void StateMachine::State::addEntryBinding(SGBinding* aBinding)
|
||||
{
|
||||
d->_entryBindings.push_back(aBinding);
|
||||
}
|
||||
|
||||
void StateMachine::State::addExitBinding(SGBinding* aBinding)
|
||||
{
|
||||
d->_exitBindings.push_back(aBinding);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
StateMachine::Transition::Transition(const std::string& aName, State* aTarget) :
|
||||
d(new TransitionPrivate)
|
||||
{
|
||||
assert(aTarget);
|
||||
d->_name = aName;
|
||||
d->_target = aTarget;
|
||||
}
|
||||
|
||||
StateMachine::Transition::~Transition()
|
||||
{
|
||||
}
|
||||
|
||||
StateMachine::State* StateMachine::Transition::target() const
|
||||
{
|
||||
return d->_target;
|
||||
}
|
||||
|
||||
void StateMachine::Transition::addSourceState(State* aSource)
|
||||
{
|
||||
if (aSource == d->_target) { // should this be disallowed outright?
|
||||
SG_LOG(SG_GENERAL, SG_WARN, d->_name << ": adding target state as source");
|
||||
}
|
||||
|
||||
d->_sourceStates.insert(aSource);
|
||||
}
|
||||
|
||||
bool StateMachine::Transition::applicableForState(State* aCurrent) const
|
||||
{
|
||||
return d->_sourceStates.count(aCurrent);
|
||||
}
|
||||
|
||||
bool StateMachine::Transition::evaluate() const
|
||||
{
|
||||
return d->_condition->test();
|
||||
}
|
||||
|
||||
void StateMachine::Transition::fireBindings()
|
||||
{
|
||||
fireBindingList(d->_bindings);
|
||||
}
|
||||
|
||||
std::string StateMachine::Transition::name() const
|
||||
{
|
||||
return d->_name;
|
||||
}
|
||||
|
||||
void StateMachine::Transition::setTriggerCondition(SGCondition* aCondition)
|
||||
{
|
||||
d->_condition = aCondition;
|
||||
}
|
||||
|
||||
void StateMachine::Transition::addBinding(SGBinding* aBinding)
|
||||
{
|
||||
d->_bindings.push_back(aBinding);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
StateMachine::StateMachine() :
|
||||
d(new StateMachinePrivate(this))
|
||||
{
|
||||
d->_root = new SGPropertyNode();
|
||||
}
|
||||
|
||||
StateMachine::~StateMachine()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void StateMachine::init()
|
||||
{
|
||||
|
||||
|
||||
d->_currentStateIndex = d->_root->getChild("current-index", 0, true);
|
||||
d->_currentStateIndex->setIntValue(0);
|
||||
|
||||
d->_currentStateName = d->_root->getChild("current-name", 0, true);
|
||||
d->_currentStateName->setStringValue("");
|
||||
|
||||
d->_currentStateIndex->addChangeListener(d.get());
|
||||
d->_currentStateName->addChangeListener(d.get());
|
||||
|
||||
d->_timeInStateProp = d->_root->getChild("elapsed-time-msec", 0, true);
|
||||
d->_timeInStateProp->setIntValue(0);
|
||||
|
||||
// TODO go to default state if found
|
||||
d->computeEligibleTransitions();
|
||||
|
||||
}
|
||||
|
||||
void StateMachine::shutdown()
|
||||
{
|
||||
d->_currentStateIndex->removeChangeListener(d.get());
|
||||
d->_currentStateName->removeChangeListener(d.get());
|
||||
|
||||
}
|
||||
|
||||
void StateMachine::innerChangeState(State_ptr aState, Transition_ptr aTrans)
|
||||
{
|
||||
d->_currentState->fireExitBindings();
|
||||
|
||||
// fire bindings before we change the state, hmmmm
|
||||
if (aTrans) {
|
||||
aTrans->fireBindings();
|
||||
}
|
||||
|
||||
// update our private state and properties
|
||||
d->_listenerLockout = true;
|
||||
d->_currentState = aState;
|
||||
d->_timeInState.stamp();
|
||||
d->_currentStateName->setStringValue(d->_currentState->name());
|
||||
d->_currentStateIndex->setIntValue(indexOfState(aState));
|
||||
d->_timeInStateProp->setIntValue(0);
|
||||
d->_listenerLockout = false;
|
||||
|
||||
// fire bindings
|
||||
d->_currentState->fireEntryBindings();
|
||||
d->_currentState->update();
|
||||
|
||||
d->computeEligibleTransitions();
|
||||
}
|
||||
|
||||
void StateMachine::changeToState(State_ptr aState, bool aOnlyIfDifferent)
|
||||
{
|
||||
assert(aState != NULL);
|
||||
if (std::find(d->_states.begin(), d->_states.end(), aState) == d->_states.end()) {
|
||||
throw sg_exception("Requested change to state not in machine");
|
||||
}
|
||||
|
||||
if (aOnlyIfDifferent && (aState == d->_currentState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
innerChangeState(aState, NULL);
|
||||
}
|
||||
|
||||
void StateMachine::changeToStateName(const std::string& aName, bool aOnlyIfDifferent)
|
||||
{
|
||||
State_ptr st = findStateByName(aName);
|
||||
if (!st) {
|
||||
throw sg_range_exception("unknown state:" + aName);
|
||||
}
|
||||
|
||||
changeToState(st, aOnlyIfDifferent);
|
||||
}
|
||||
|
||||
StateMachine::State_ptr StateMachine::state() const
|
||||
{
|
||||
return d->_currentState;
|
||||
}
|
||||
|
||||
SGPropertyNode* StateMachine::root()
|
||||
{
|
||||
return d->_root;
|
||||
}
|
||||
|
||||
void StateMachine::update(double aDt)
|
||||
{
|
||||
// do this first, for triggers which depend on time in current state
|
||||
// (spring-loaded transitions)
|
||||
d->_timeInStateProp->setIntValue(d->_timeInState.elapsedMSec());
|
||||
|
||||
Transition_ptr trigger;
|
||||
|
||||
BOOST_FOREACH(Transition* trans, d->_eligible) {
|
||||
if (trans->evaluate()) {
|
||||
if (trigger != Transition_ptr()) {
|
||||
SG_LOG(SG_GENERAL, SG_WARN, "ambiguous transitions! "
|
||||
<< trans->name() << " or " << trigger->name());
|
||||
}
|
||||
|
||||
trigger = trans;
|
||||
}
|
||||
}
|
||||
|
||||
if (trigger != Transition_ptr()) {
|
||||
SG_LOG(SG_GENERAL, SG_DEBUG, "firing transition:" << trigger->name());
|
||||
innerChangeState(trigger->target(), trigger);
|
||||
}
|
||||
|
||||
d->_currentState->update();
|
||||
}
|
||||
|
||||
StateMachine::State_ptr StateMachine::findStateByName(const std::string& aName) const
|
||||
{
|
||||
BOOST_FOREACH(State_ptr sp, d->_states) {
|
||||
if (sp->name() == aName) {
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
|
||||
SG_LOG(SG_GENERAL, SG_WARN, "unknown state:" << aName);
|
||||
return State_ptr();
|
||||
}
|
||||
|
||||
StateMachine::State_ptr StateMachine::stateByIndex(unsigned int aIndex) const
|
||||
{
|
||||
if (aIndex >= d->_states.size()) {
|
||||
throw sg_range_exception("invalid state index, out of bounds");
|
||||
}
|
||||
|
||||
return d->_states[aIndex];
|
||||
}
|
||||
|
||||
int StateMachine::indexOfState(State_ptr aState) const
|
||||
{
|
||||
StatePtrVec::const_iterator it = std::find(d->_states.begin(), d->_states.end(), aState);
|
||||
if (it == d->_states.end()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return it - d->_states.begin();
|
||||
}
|
||||
|
||||
StateMachine::State_ptr StateMachine::createState(const std::string& aName)
|
||||
{
|
||||
if (findStateByName(aName) != NULL) {
|
||||
throw sg_range_exception("duplicate state name");
|
||||
}
|
||||
|
||||
State_ptr st = new State(aName);
|
||||
addState(st);
|
||||
return st;
|
||||
}
|
||||
|
||||
StateMachine::Transition_ptr
|
||||
StateMachine::createTransition(const std::string& aName, State_ptr aTarget)
|
||||
{
|
||||
Transition_ptr t = new Transition(aName, aTarget);
|
||||
addTransition(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
StateMachine* StateMachine::createFromPlist(SGPropertyNode* desc, SGPropertyNode* root)
|
||||
{
|
||||
StateMachine* sm = new StateMachine;
|
||||
|
||||
|
||||
BOOST_FOREACH(SGPropertyNode* stateDesc, desc->getChildren("state")) {
|
||||
std::string nm = stateDesc->getStringValue("name");
|
||||
State_ptr st(new State(nm));
|
||||
|
||||
readBindingList(stateDesc, "enter", root, st->d->_updateBindings);
|
||||
readBindingList(stateDesc, "exit", root, st->d->_entryBindings);
|
||||
readBindingList(stateDesc, "update", root, st->d->_exitBindings);
|
||||
|
||||
sm->addState(st);
|
||||
} // of states iteration
|
||||
|
||||
BOOST_FOREACH(SGPropertyNode* tDesc, desc->getChildren("transition")) {
|
||||
std::string nm = tDesc->getStringValue("name");
|
||||
State_ptr target = sm->findStateByName(tDesc->getStringValue("target"));
|
||||
|
||||
SGCondition* cond = sgReadCondition(root, tDesc->getChild("condition"));
|
||||
|
||||
Transition_ptr t(new Transition(nm, target));
|
||||
t->setTriggerCondition(cond);
|
||||
|
||||
BOOST_FOREACH(SGPropertyNode* src, desc->getChildren("source")) {
|
||||
State_ptr srcState = sm->findStateByName(src->getStringValue());
|
||||
t->addSourceState(srcState);
|
||||
}
|
||||
|
||||
readBindingList(tDesc, "binding", root, t->d->_bindings);
|
||||
|
||||
sm->addTransition(t);
|
||||
} // of states iteration
|
||||
|
||||
return sm;
|
||||
}
|
||||
|
||||
void StateMachine::addState(State_ptr aState)
|
||||
{
|
||||
bool wasEmpty = d->_states.empty();
|
||||
d->_states.push_back(aState);
|
||||
if (wasEmpty) {
|
||||
d->_currentState = aState;
|
||||
}
|
||||
}
|
||||
|
||||
void StateMachine::addTransition(Transition_ptr aTrans)
|
||||
{
|
||||
d->_transitions.push_back(aTrans);
|
||||
}
|
||||
|
||||
} // of namespace simgear
|
166
simgear/structure/StateMachine.hxx
Normal file
166
simgear/structure/StateMachine.hxx
Normal file
@ -0,0 +1,166 @@
|
||||
/* -*-c++-*-
|
||||
*
|
||||
* Copyright (C) 2013 James Turner
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SIMGEAR_STATE_MACHINE_H
|
||||
#define SIMGEAR_STATE_MACHINE_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <simgear/structure/SGReferenced.hxx>
|
||||
#include <simgear/structure/SGSharedPtr.hxx>
|
||||
|
||||
// forward decls
|
||||
class SGPropertyNode;
|
||||
class SGBinding;
|
||||
class SGCondition;
|
||||
|
||||
namespace simgear
|
||||
{
|
||||
|
||||
class StateMachine : public SGReferenced
|
||||
{
|
||||
public:
|
||||
StateMachine();
|
||||
virtual ~StateMachine();
|
||||
|
||||
class State : public SGReferenced
|
||||
{
|
||||
public:
|
||||
virtual ~State();
|
||||
|
||||
std::string name() const;
|
||||
|
||||
void addUpdateBinding(SGBinding* aBinding);
|
||||
void addEntryBinding(SGBinding* aBinding);
|
||||
void addExitBinding(SGBinding* aBinding);
|
||||
|
||||
private:
|
||||
friend class StateMachine;
|
||||
|
||||
State(const std::string& name);
|
||||
|
||||
void fireExitBindings();
|
||||
void fireEntryBindings();
|
||||
|
||||
void update();
|
||||
|
||||
class StatePrivate;
|
||||
std::auto_ptr<StatePrivate> d;
|
||||
};
|
||||
|
||||
class Transition : public SGReferenced
|
||||
{
|
||||
public:
|
||||
virtual ~Transition();
|
||||
|
||||
std::string name() const;
|
||||
|
||||
/**
|
||||
* The state we end in, after this transition fires
|
||||
*/
|
||||
State* target() const;
|
||||
|
||||
/**
|
||||
* Add a state in which this transition is eligible to fire
|
||||
*/
|
||||
void addSourceState(State* aSource);
|
||||
|
||||
/**
|
||||
* Specify the transition trigger condition. Takes ownership
|
||||
*/
|
||||
void setTriggerCondition(SGCondition* aCondition);
|
||||
|
||||
|
||||
void addBinding(SGBinding* aBinding);
|
||||
private:
|
||||
friend class StateMachine;
|
||||
|
||||
Transition(const std::string& aName, State* aTarget);
|
||||
|
||||
/**
|
||||
* predicate to determine if this transition can fire given a
|
||||
* current state.
|
||||
*/
|
||||
bool applicableForState(State* aCurrent) const;
|
||||
|
||||
/**
|
||||
* test if the transition should fire, based on current state
|
||||
*/
|
||||
bool evaluate() const;
|
||||
|
||||
void fireBindings();
|
||||
|
||||
class TransitionPrivate;
|
||||
std::auto_ptr<TransitionPrivate> d;
|
||||
};
|
||||
|
||||
typedef SGSharedPtr<State> State_ptr;
|
||||
typedef SGSharedPtr<Transition> Transition_ptr;
|
||||
|
||||
/**
|
||||
* create a state machine from a property list description
|
||||
*/
|
||||
static StateMachine* createFromPlist(SGPropertyNode* desc, SGPropertyNode* root);
|
||||
|
||||
SGPropertyNode* root();
|
||||
|
||||
void init();
|
||||
void shutdown();
|
||||
|
||||
void update(double dt);
|
||||
|
||||
State_ptr state() const;
|
||||
|
||||
/**
|
||||
* public API to force a change to a particular state.
|
||||
* @param aOnlyIfDifferent - only make a transition if the new state is
|
||||
* different from the current state. Otherwise, the existing state will
|
||||
* be exited and re-entered.
|
||||
*/
|
||||
void changeToState(State_ptr aState, bool aOnlyIfDifferent=true);
|
||||
|
||||
/// wrapper to change state by looking up a name
|
||||
void changeToStateName(const std::string& aName, bool aOnlyIfDifferent=true);
|
||||
|
||||
State_ptr findStateByName(const std::string& aName) const;
|
||||
|
||||
State_ptr stateByIndex(unsigned int aIndex) const;
|
||||
|
||||
int indexOfState(State_ptr aState) const;
|
||||
|
||||
// programatic creation
|
||||
State_ptr createState(const std::string& aName);
|
||||
Transition_ptr createTransition(const std::string& aName, State_ptr aTarget);
|
||||
private:
|
||||
void addState(State_ptr aState);
|
||||
void addTransition(Transition_ptr aTrans);
|
||||
|
||||
void innerChangeState(State_ptr aState, Transition_ptr aTrans);
|
||||
|
||||
class StateMachinePrivate;
|
||||
std::auto_ptr<StateMachinePrivate> d;
|
||||
};
|
||||
|
||||
typedef SGSharedPtr<StateMachine> StateMachine_ptr;
|
||||
|
||||
} // of simgear namespace
|
||||
|
||||
#endif // of SIMGEAR_STATE_MACHINE_H
|
226
simgear/structure/state_machine_test.cxx
Normal file
226
simgear/structure/state_machine_test.cxx
Normal file
@ -0,0 +1,226 @@
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <simgear_config.h>
|
||||
#endif
|
||||
|
||||
#ifdef NDEBUG
|
||||
// Always enable DEBUG mode in test application, otherwise "assert" test
|
||||
// statements have no effect and don't actually test anything (catch 17 ;-) ).
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
|
||||
#include <simgear/compiler.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "StateMachine.hxx"
|
||||
|
||||
#include <simgear/structure/SGBinding.hxx>
|
||||
#include <simgear/structure/exception.hxx>
|
||||
#include <simgear/props/condition.hxx>
|
||||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/props/props_io.hxx>
|
||||
#include <simgear/structure/commands.hxx>
|
||||
|
||||
using std::string;
|
||||
using std::cout;
|
||||
using std::cerr;
|
||||
using std::endl;
|
||||
|
||||
// SGCondition subclass we can trivially manipulate from test code.
|
||||
class DummyCondition : public SGCondition
|
||||
{
|
||||
public:
|
||||
DummyCondition(): _state(false) { }
|
||||
|
||||
virtual bool test() const
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
bool _state;
|
||||
};
|
||||
|
||||
static int dummy_cmd_state = 0;
|
||||
|
||||
bool dummyCommand(const SGPropertyNode* arg)
|
||||
{
|
||||
++dummy_cmd_state;
|
||||
return true;
|
||||
}
|
||||
|
||||
#define COMPARE(a, b) \
|
||||
if ((a) != (b)) { \
|
||||
cerr << "failed:" << #a << " != " << #b << endl; \
|
||||
cerr << "\tgot:'" << a << "'" << endl; \
|
||||
exit(1); \
|
||||
}
|
||||
|
||||
#define VERIFY(a) \
|
||||
if (!(a)) { \
|
||||
cerr << "failed:" << #a << endl; \
|
||||
exit(1); \
|
||||
}
|
||||
|
||||
using namespace simgear;
|
||||
|
||||
#define BUILD_MACHINE_1() \
|
||||
StateMachine_ptr sm(new StateMachine); \
|
||||
StateMachine::State_ptr stateA = sm->createState("a"); \
|
||||
StateMachine::State_ptr stateB = sm->createState("b"); \
|
||||
StateMachine::State_ptr stateC = sm->createState("c"); \
|
||||
\
|
||||
DummyCondition* trigger1 = new DummyCondition; \
|
||||
StateMachine::Transition_ptr t1 = sm->createTransition(">b", stateB); \
|
||||
t1->addSourceState(stateA); \
|
||||
t1->setTriggerCondition(trigger1); \
|
||||
\
|
||||
DummyCondition* trigger2 = new DummyCondition; \
|
||||
StateMachine::Transition_ptr t2 = sm->createTransition(">c", stateC); \
|
||||
t2->addSourceState(stateB); \
|
||||
t2->setTriggerCondition(trigger2); \
|
||||
\
|
||||
DummyCondition* trigger3 = new DummyCondition; \
|
||||
StateMachine::Transition_ptr t3 = sm->createTransition(">a", stateA); \
|
||||
t3->addSourceState(stateC); \
|
||||
t3->addSourceState(stateB); \
|
||||
t3->setTriggerCondition(trigger3); \
|
||||
sm->init();
|
||||
|
||||
void testBasic()
|
||||
{
|
||||
BUILD_MACHINE_1();
|
||||
////////////////////////////////////////////
|
||||
COMPARE(sm->state()->name(), "a");
|
||||
|
||||
COMPARE(sm->indexOfState(stateA), 0);
|
||||
COMPARE(sm->findStateByName("c"), stateC);
|
||||
|
||||
sm->changeToState(stateC);
|
||||
COMPARE(sm->state(), stateC);
|
||||
|
||||
trigger3->_state = true;
|
||||
sm->update(1.0);
|
||||
COMPARE(sm->state()->name(), "a");
|
||||
trigger3->_state = false;
|
||||
|
||||
trigger1->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger1->_state = false;
|
||||
COMPARE(sm->state()->name(), "b");
|
||||
|
||||
trigger3->_state = true;
|
||||
sm->update(1.0);
|
||||
COMPARE(sm->state()->name(), "a");
|
||||
trigger3->_state = false;
|
||||
|
||||
trigger1->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger1->_state = false;
|
||||
COMPARE(sm->state()->name(), "b");
|
||||
|
||||
trigger2->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger2->_state = false;
|
||||
COMPARE(sm->state()->name(), "c");
|
||||
|
||||
//////////////////////////////////////////
|
||||
COMPARE(sm->root()->getIntValue("current-index"), 2);
|
||||
COMPARE(sm->root()->getStringValue("current-name"), string("c"));
|
||||
|
||||
sm->root()->setStringValue("current-name", "b");
|
||||
COMPARE(sm->state()->name(), "b");
|
||||
|
||||
////////////////////////////////////////
|
||||
COMPARE(sm->findStateByName("foo"), NULL);
|
||||
COMPARE(sm->indexOfState(StateMachine::State_ptr()), -1);
|
||||
|
||||
COMPARE(sm->stateByIndex(1), stateB);
|
||||
|
||||
try {
|
||||
sm->stateByIndex(44);
|
||||
VERIFY(false && "should have raised an exception");
|
||||
} catch (sg_exception& e){
|
||||
// expected!
|
||||
}
|
||||
}
|
||||
|
||||
void testBindings()
|
||||
{
|
||||
SGCommandMgr* cmdMgr = SGCommandMgr::instance();
|
||||
cmdMgr->addCommand("dummy", dummyCommand);
|
||||
BUILD_MACHINE_1();
|
||||
|
||||
t2->addBinding(new SGBinding("dummy"));
|
||||
|
||||
stateA->addEntryBinding(new SGBinding("dummy"));
|
||||
stateA->addExitBinding(new SGBinding("dummy"));
|
||||
stateC->addEntryBinding(new SGBinding("dummy"));
|
||||
|
||||
////////////////////////
|
||||
COMPARE(sm->state()->name(), "a");
|
||||
trigger1->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger1->_state = false;
|
||||
COMPARE(sm->state()->name(), "b");
|
||||
COMPARE(dummy_cmd_state, 1); // exit state A
|
||||
|
||||
trigger2->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger2->_state = false;
|
||||
COMPARE(dummy_cmd_state, 3); // fire transition 2, enter state C
|
||||
|
||||
dummy_cmd_state = 0;
|
||||
sm->changeToState(stateA);
|
||||
COMPARE(dummy_cmd_state, 1); // enter state A
|
||||
trigger1->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger1->_state = false;
|
||||
COMPARE(dummy_cmd_state, 2); // exit state A
|
||||
|
||||
////////////////////////
|
||||
t3->addBinding(new SGBinding("dummy"));
|
||||
t3->addBinding(new SGBinding("dummy"));
|
||||
t3->addBinding(new SGBinding("dummy"));
|
||||
|
||||
sm->changeToStateName("b");
|
||||
dummy_cmd_state = 0;
|
||||
trigger3->_state = true;
|
||||
sm->update(1.0);
|
||||
trigger3->_state = false;
|
||||
COMPARE(dummy_cmd_state, 4); // three transition bindings, enter A
|
||||
}
|
||||
|
||||
void testParse()
|
||||
{
|
||||
const char* xml = "<?xml version=\"1.0\"?>"
|
||||
"<PropertyList>"
|
||||
"<state>"
|
||||
"<name>one</name>"
|
||||
"</state>"
|
||||
"<state>"
|
||||
"<name>two</name>"
|
||||
"</state>"
|
||||
"</PropertyList>";
|
||||
|
||||
SGPropertyNode* desc = new SGPropertyNode;
|
||||
readProperties(xml, strlen(xml), desc);
|
||||
|
||||
SGPropertyNode_ptr root(new SGPropertyNode);
|
||||
StateMachine_ptr sm = StateMachine::createFromPlist(desc, root);
|
||||
|
||||
VERIFY(sm->findStateByName("one") != NULL);
|
||||
VERIFY(sm->findStateByName("two") != NULL);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
testBasic();
|
||||
testBindings();
|
||||
testParse();
|
||||
cout << __FILE__ << ": All tests passed" << endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user