diff --git a/direct/src/fsm/ClassicFSM.py b/direct/src/fsm/ClassicFSM.py new file mode 100644 index 0000000000..96baaa3013 --- /dev/null +++ b/direct/src/fsm/ClassicFSM.py @@ -0,0 +1,368 @@ +"""Finite State Machine module: contains the ClassicFSM class. + +This module and class exist only for backward compatibility with +existing code. New code should use the FSM module instead. + +""" + +from DirectObject import * +import types + +class ClassicFSM(DirectObject): + """ClassicFSM class: Finite State Machine class. + + This module and class exist only for backward compatibility with + existing code. New code should use the FSM class instead. + """ + + # create ClassicFSM DirectNotify category + notify = directNotify.newCategory("ClassicFSM") + + # special methods + + # these are flags that tell the ClassicFSM what to do when an + # undefined transition is requested: + ALLOW = 0 # print a warning, and do the transition + DISALLOW = 1 # silently ignore the request (don't do the transition) + DISALLOW_VERBOSE = 2 # print a warning, and don't do the transition + ERROR = 3 # print an error message and raise an exception + + def __init__(self, name, states=[], initialStateName=None, + finalStateName=None, onUndefTransition=DISALLOW_VERBOSE): + """__init__(self, string, State[], string, string, int) + + ClassicFSM constructor: takes name, list of states, initial state and + final state as: + + fsm = ClassicFSM.ClassicFSM('stopLight', + [ State.State('red', enterRed, exitRed, ['green']), + State.State('yellow', enterYellow, exitYellow, ['red']), + State.State('green', enterGreen, exitGreen, ['yellow']) ], + 'red', + 'red') + + each state's last argument, a list of allowed state transitions, + is optional; if left out (or explicitly specified to be + State.State.Any) then any transition from the state is 'defined' + and allowed + + 'onUndefTransition' flag determines behavior when undefined + transition is requested; see flag definitions above + + """ + + self.setName(name) + self.setStates(states) + self.setInitialState(initialStateName) + self.setFinalState(finalStateName) + + self.onUndefTransition = onUndefTransition + + # Flag to see if we are inspecting + self.inspecting = 0 + + # We do not enter the initial state to separate + # construction from activation + self.__currentState = None + + # We set this while we are modifying the state. No one else + # should recursively attempt to modify the state while we are + # doing this. + self.__internalStateInFlux = 0 + + # I know this isn't how __repr__ is supposed to be used, but it + # is nice and convenient. + def __repr__(self): + return self.__str__() + + def __str__(self): + """__str__(self) + Print out something useful about the fsm + """ + currentState = self.getCurrentState() + if currentState: + str = ("ClassicFSM " + self.getName() + ' in state "' + + currentState.getName() + '"') + else: + str = ("ClassicFSM " + self.getName() + ' not in any state') + return str + + def enterInitialState(self, argList=[]): + assert(not self.__internalStateInFlux) + if self.__currentState == self.__initialState: + return + + assert(self.__currentState == None) + self.__internalStateInFlux = 1 + self.__enter(self.__initialState, argList) + assert(not self.__internalStateInFlux) + + # Jesse decided that simpler was better with the __str__ function + def __str_not__(self): + """__str__(self)""" + return "ClassicFSM: name = %s \n states = %s \n initial = %s \n final = %s \n current = %s" \ + % (self.__name, self.__states, self.__initialState, + self.__finalState, self.__currentState) + + + # setters and getters + + def getName(self): + """getName(self)""" + return(self.__name) + + def setName(self, name): + """setName(self, string)""" + self.__name = name + + def getStates(self): + """getStates(self)""" + return(self.__states) + + def setStates(self, states): + """setStates(self, State[])""" + self.__states = states + + def addState(self, state): + """addState(state)""" + self.__states.append(state) + + def getInitialState(self): + """getInitialState(self)""" + return(self.__initialState) + + def setInitialState(self, initialStateName): + """setInitialState(self, string)""" + self.__initialState = self.getStateNamed(initialStateName) + + def getFinalState(self): + """getFinalState(self)""" + return(self.__finalState) + + def setFinalState(self, finalStateName): + """setFinalState(self, string)""" + self.__finalState = self.getStateNamed(finalStateName) + + def requestFinalState(self): + self.request(self.__finalState.getName()) + + def getCurrentState(self): + """getCurrentState(self)""" + return(self.__currentState) + + + # lookup funcs + + def getStateNamed(self, stateName): + """getStateNamed(self, string) + Return the state with given name if found, issue warning otherwise""" + for state in self.__states: + if (state.getName() == stateName): + return state + ClassicFSM.notify.warning("[%s] : getStateNamed: %s, no such state" % + (self.__name, str(stateName))) + + + # basic ClassicFSM functionality + + def __exitCurrent(self, argList): + """__exitCurrent(self) + Exit the current state""" + assert(self.__internalStateInFlux) + if ClassicFSM.notify.getDebug(): + ClassicFSM.notify.debug("[%s]: exiting %s" % (self.__name, + self.__currentState.getName())) + self.__currentState.exit(argList) + # Only send the state change event if we are inspecting it + # If this event turns out to be generally useful, we can + # turn it on all the time, but for now nobody else is using it + if self.inspecting: + messenger.send(self.getName() + '_' + + self.__currentState.getName() + '_exited') + self.__currentState = None + + def __enter(self, aState, argList=[]): + """__enter(self, State) + Enter a given state, if it exists""" + assert(self.__internalStateInFlux) + if (aState in self.__states): + if ClassicFSM.notify.getDebug(): + ClassicFSM.notify.debug("[%s]: entering %s" % (self.__name, + aState.getName())) + self.__currentState = aState + # Only send the state change event if we are inspecting it + # If this event turns out to be generally useful, we can + # turn it on all the time, but for now nobody else is using it + if self.inspecting: + messenger.send(self.getName() + '_' + + aState.getName() + '_entered') + + # Once we begin entering the new state, we're allow to + # recursively request a transition to another state. + # Indicate this by marking our internal state no longer in + # flux. + self.__internalStateInFlux = 0 + aState.enter(argList) + else: + # notify.error is going to raise an exception; reset the + # flux flag first + self.__internalStateInFlux = 0 + ClassicFSM.notify.error("[%s]: enter: no such state" % (self.__name)) + + def __transition(self, aState, enterArgList=[], exitArgList=[]): + """__transition(self, State, enterArgList, exitArgList) + Exit currentState and enter given one""" + assert(not self.__internalStateInFlux) + self.__internalStateInFlux = 1 + self.__exitCurrent(exitArgList) + self.__enter(aState, enterArgList) + assert(not self.__internalStateInFlux) + + def request(self, aStateName, enterArgList=[], exitArgList=[], + force=0): + """request(self, string) + Attempt transition from currentState to given one. + Return true is transition exists to given state, + false otherwise. + """ + + # If you trigger this assertion failure, you must have + # recursively requested a state transition from within the + # exitState() function for the previous state. This is not + # supported because we're not fully transitioned into the new + # state yet. + assert(not self.__internalStateInFlux) + + if not self.__currentState: + # Make this a warning for now + ClassicFSM.notify.warning("[%s]: request: never entered initial state" % + (self.__name)) + self.__currentState = self.__initialState + + if isinstance(aStateName, types.StringType): + aState = self.getStateNamed(aStateName) + else: + # Allow the caller to pass in a state in itself, not just + # the name of a state. + aState = aStateName + aStateName = aState.getName() + + if aState == None: + ClassicFSM.notify.error("[%s]: request: %s, no such state" % + (self.__name, aStateName)) + + # is the transition defined? if it isn't, should we allow it? + transitionDefined = self.__currentState.isTransitionDefined(aStateName) + transitionAllowed = transitionDefined + + if self.onUndefTransition == ClassicFSM.ALLOW: + transitionAllowed = 1 + if not transitionDefined: + # the transition is not defined, but we're going to do it + # anyway. print a warning. + ClassicFSM.notify.warning( + "[%s]: performing undefined transition from %s to %s" % + (self.__name, + self.__currentState.getName(), + aStateName)) + + if transitionAllowed or force: + self.__transition(aState, + enterArgList, + exitArgList) + return 1 + # We can implicitly always transition to our final state. + elif (aStateName == self.__finalState.getName()): + if (self.__currentState == self.__finalState): + # Do not do the transition if we are already in the final state + if ClassicFSM.notify.getDebug(): + ClassicFSM.notify.debug("[%s]: already in final state: %s" % + (self.__name, aStateName)) + return 1 + else: + # Force a transition to allow for cleanup + if ClassicFSM.notify.getDebug(): + ClassicFSM.notify.debug("[%s]: implicit transition to final state: %s" % + (self.__name, aStateName)) + self.__transition(aState, + enterArgList, + exitArgList) + return 1 + # are we already in this state? + elif (aStateName == self.__currentState.getName()): + if ClassicFSM.notify.getDebug(): + ClassicFSM.notify.debug("[%s]: already in state %s and no self transition" % + (self.__name, aStateName)) + return 0 + else: + msg = ("[%s]: no transition exists from %s to %s" % + (self.__name, + self.__currentState.getName(), + aStateName)) + if self.onUndefTransition == ClassicFSM.ERROR: + ClassicFSM.notify.error(msg) + elif self.onUndefTransition == ClassicFSM.DISALLOW_VERBOSE: + ClassicFSM.notify.warning(msg) + return 0 + + + def forceTransition(self, aStateName, enterArgList=[], exitArgList=[]): + """ force a transition -- for debugging ONLY """ + self.request(aStateName, enterArgList, exitArgList, force=1) + + def conditional_request(self, aStateName, enterArgList=[], exitArgList=[]): + """request(self, string) + 'if this transition is defined, do it' + Attempt transition from currentState to given one, if it exists. + Return true if transition exists to given state, + false otherwise. It is NOT an error/warning to attempt a cond_request + if the transition doesn't exist. This lets people be sloppy about + ClassicFSM transitions, letting the same fn be used for different states + that may not have the same out transitions. + """ + + assert(not self.__internalStateInFlux) + if not self.__currentState: + # Make this a warning for now + ClassicFSM.notify.warning("[%s]: request: never entered initial state" % + (self.__name)) + self.__currentState = self.__initialState + + if isinstance(aStateName, types.StringType): + aState = self.getStateNamed(aStateName) + else: + # Allow the caller to pass in a state in itself, not just + # the name of a state. + aState = aStateName + aStateName = aState.getName() + + if aState == None: + ClassicFSM.notify.error("[%s]: request: %s, no such state" % + (self.__name, aStateName)) + + transitionDefined = ( + self.__currentState.isTransitionDefined(aStateName) or + aStateName in [self.__currentState.getName(), + self.__finalState.getName()] + ) + + if transitionDefined: + return self.request(aStateName, enterArgList, exitArgList) + else: + ClassicFSM.notify.debug("[%s]: condition_request: %s, transition doesnt exist" % + (self.__name, aStateName)) + return 0 + + def view(self): + import FSMInspector + FSMInspector.FSMInspector(self) + + + + + + + + + + diff --git a/direct/src/fsm/FSM.py b/direct/src/fsm/FSM.py index 4e1dfeddd4..e14ab9edfe 100644 --- a/direct/src/fsm/FSM.py +++ b/direct/src/fsm/FSM.py @@ -1,68 +1,248 @@ -"""Finite State Machine module: contains the FSM class""" +"""The new Finite State Machine module. This replaces the modules +previously called FSM.py (now called ClassicFSM.py). +""" -from DirectObject import * +import DirectObject +import DirectNotifyGlobal import types +import string -class FSM(DirectObject): - """FSM class: Finite State Machine class""" +class FSM(DirectObject.DirectObject): + """A Finite State Machine. This is intended to be the base class + of any number of specific machines, which consist of a collection + of states and transitions, and rules to switch between states + according to arbitrary input data. - # create FSM DirectNotify category - notify = directNotify.newCategory("FSM") + The states of an FSM are defined implicitly. Each state is + identified by a string, which by convention begins with a capital + letter. (Also by convention, strings passed to request that are + not state names should begin with a lowercase letter.) - # special methods + To define specialized behavior when entering or exiting a + particular state, define a method named enterState() and/or + exitState(), where "State" is the name of the state, e.g.: - # these are flags that tell the FSM what to do when an - # undefined transition is requested: - ALLOW = 0 # print a warning, and do the transition - DISALLOW = 1 # silently ignore the request (don't do the transition) - DISALLOW_VERBOSE = 2 # print a warning, and don't do the transition - ERROR = 3 # print an error message and raise an exception + def enterRed(self, oldState, newState): + ... do stuff ... - def __init__(self, name, states=[], initialStateName=None, - finalStateName=None, onUndefTransition=DISALLOW_VERBOSE): - """__init__(self, string, State[], string, string, int) + def exitRed(self, oldState, newState): + ... cleanup stuff ... - FSM constructor: takes name, list of states, initial state and - final state as: + def enterYellow(self, oldState, newState): + ... do stuff ... - fsm = FSM.FSM('stopLight', - [ State.State('red', enterRed, exitRed, ['green']), - State.State('yellow', enterYellow, exitYellow, ['red']), - State.State('green', enterGreen, exitGreen, ['yellow']) ], - 'red', - 'red') + def exitYellow(self, oldState, newState): + ... cleanup stuff ... - each state's last argument, a list of allowed state transitions, - is optional; if left out (or explicitly specified to be - State.State.Any) then any transition from the state is 'defined' - and allowed + def enterGreen(self, oldState, newState): + ... do stuff ... - 'onUndefTransition' flag determines behavior when undefined - transition is requested; see flag definitions above + def exitGreen(self, oldState, newState): + ... cleanup stuff ... - """ + Both functions are supplied the previous state name and the new + state name we are transitioning to. (Of course, the newState + passed to enterRed, and the oldState passed to exitRed, will + always be "Red".) - self.setName(name) - self.setStates(states) - self.setInitialState(initialStateName) - self.setFinalState(finalStateName) + Both functions are optional. If either function is omitted, the + state is still defined, but nothing is done when transitioning + into (or out of) the state. - self.onUndefTransition = onUndefTransition + Additionally, you may define a filterState() function for each + state. The purpose of this function is to decide what state to + transition to next, if any, on receipt of a particular input. The + input is always a string and a tuple of optional parameters (which + is often empty), and the return value should either be None to do + nothing, or the name of the state to transition into. For + example: - # Flag to see if we are inspecting - self.inspecting = 0 + def filterRed(self, request, args): + if request in ['Green']: + return request + return None - # We do not enter the initial state to separate - # construction from activation - self.__currentState = None + def filterYellow(self, request, args): + if request in ['Red']: + return request + return None - # We set this while we are modifying the state. No one else - # should recursively attempt to modify the state while we are - # doing this. - self.__internalStateInFlux = 0 + def filterGreen(self, request, args): + if request in ['Yellow']: + return request + return None - # I know this isn't how __repr__ is supposed to be used, but it - # is nice and convenient. + As above, the filterState() functions are optional. If any is + omitted, the defaultFilter() method is called instead. The + default definition of defaultFilter() always returns None, thus + disallowing all unexpected transitions. This default behavior may + be overridden in a derived class. + + FSM.state may be queried at any time other than during the + handling of the enter() and exit() functions. During these + functions, FSM.state contains the value None (you are not really + in any state during the transition). At other times, FSM.state + contains the name of the current state. + + Initially, the FSM is in state 'Off'. It does not call enterOff() + at construction time; it is simply in Off already by convention. + If you need to call code in enterOff() to initialize your FSM + properly, call it explicitly in the constructor. Similarly, when + cleanup() is called or the FSM is destructed, the FSM transitions + back to 'Off' by convention. (It does call enterOff() at this + point, but does not call exitOff().) + + To implement nested hierarchical FSM's, simply create a nested FSM + and store it on the class within the appropriate enterState() + function, and clean it up within the corresponding exitState() + function. + + See the code in SampleFSM.py for further examples. + """ + + notify = DirectNotifyGlobal.directNotify.newCategory("FSM") + + def __init__(self, name): + self.name = name + # Initially, we are in the Off state by convention. + self.state = 'Off' + + # This member lists the default transitions that are accepted + # without question by the defaultFilter. It's a map of state + # names to a list of legal target state names from that state. + # Define it only if you want to use the classic FSM model of + # defining all (or most) of your transitions up front. If + # this is set to None (the default), all named-state + # transitions (that is, those requests whose name begins with + # a capital letter) are allowed. If it is set to an empty + # map, no transitions are implicitly allowed--all transitions + # must be approved by some filter function. + self.defaultTransitions = None + + def __del__(self): + self.cleanup() + + def cleanup(self): + # A convenience function to force the FSM to clean itself up + # by transitioning to the "Off" state. + assert(self.state) + if self.state != 'Off': + self.__setState('Off') + + def forceTransition(self, newState): + """Changes unconditionally to the indicated state. This + bypasses the filterState() function, and just calls + exitState() followed by enterState().""" + + assert(isinstance(newState, types.StringType)) + + self.__setState(newState) + + def request(self, request, *args): + """Requests a state transition (or other behavior). The request + parameter should be a string. The request, along with any + additional arguments, is passed to the current filterState() + function. If filterState() returns a string, the FSM + transitions to that state. + + The return value is the same as the return value of + filterState() (that is, None if the request does not provoke a + state transition, or the name of the new state otherwise.) + + If the FSM is currently in transition (i.e. in the middle of + executing an enterState or exitState function), the request is + denied and None is returned.""" + + assert(isinstance(request, types.StringType)) + self.notify.debug("%s.request(%s, %s" % (self.name, request, str(args)[1:])) + + if not self.state: + self.notify.warning("rejecting request %s while FSM is in transition." % (request)) + return None + + func = getattr(self, "filter" + self.state, None) + if not func: + # If there's no matching filterState() function, call + # defaultFilter() instead. + func = self.defaultFilter + result = func(request, args) + if result: + assert(isinstance(result, types.StringType)) + self.__setState(result) + + return result + + def defaultFilter(self, request, args): + """This is the function that is called if there is no + filterState() method for a particular state name. By default, + the filter defined here in the base class always returns + None, disallowing any transition. Specialized FSM's may wish + to redefine this default filter (for instance, to always + return the request itself, thus allowing any transition.).""" + + if request == 'Off': + # We can always go to the "Off" state. + return request + + if self.defaultTransitions is None: + # If self.defaultTransitions is None, it means to accept + # all requests whose name begins with a capital letter. + # These are direct requests to a particular state. + if request[0] in string.uppercase: + return request + + else: + # If self.defaultTransitions is not None, it is a map of + # allowed transitions from each state. That is, each key + # of the map is the current state name; for that key, the + # value is a list of allowed transitions from the + # indicated state. + if request in self.defaultTransitions.get(self.state, []): + # This transition is listed in the defaultTransitions map; + # accept it. + return request + + # If self.defaultTransitions is not None, it is an error + # to request a direct state transition (capital letter + # request) not listed in defaultTransitions and not + # handled by an earlier filter. + if request[0] in string.uppercase: + self.notify.error("%s rejecting request %s from state %s." % (self.name, request, self.state)) + + # In either case, we quietly ignore unhandled command + # (lowercase) requests. + assert(self.notify.debug("%s ignoring request %s from state %s." % (self.name, request, self.state))) + return None + + def filterOff(self, request, args): + """From the off state, we can always go directly to any other + state.""" + if request[0] in string.uppercase: + return request + return self.defaultFilter(request, args) + + + def __setState(self, newState): + # Internal function to change unconditionally to the indicated + # state. + assert(self.state) + + oldState = self.state + self.state = None + self.__callTransitionFunc("exit" + oldState, oldState, newState) + self.__callTransitionFunc("enter" + newState, oldState, newState) + self.state = newState + + + def __callTransitionFunc(self, name, oldState, newState): + # Calls the appropriate enter or exit function when + # transitioning between states, if it exists. + assert(self.state == None) + + func = getattr(self, name, None) + if func: + func(oldState, newState) + def __repr__(self): return self.__str__() @@ -70,290 +250,8 @@ class FSM(DirectObject): """__str__(self) Print out something useful about the fsm """ - currentState = self.getCurrentState() - if currentState: - str = ("FSM " + self.getName() + ' in state "' + - currentState.getName() + '"') + if self.state: + str = ("FSM " + self.name + ' in state "' + self.state + '"') else: - str = ("FSM " + self.getName() + ' not in any state') + str = ("FSM " + self.name + ' not in any state') return str - - def enterInitialState(self, argList=[]): - assert(not self.__internalStateInFlux) - if self.__currentState == self.__initialState: - return - - assert(self.__currentState == None) - self.__internalStateInFlux = 1 - self.__enter(self.__initialState, argList) - assert(not self.__internalStateInFlux) - - # Jesse decided that simpler was better with the __str__ function - def __str_not__(self): - """__str__(self)""" - return "FSM: name = %s \n states = %s \n initial = %s \n final = %s \n current = %s" \ - % (self.__name, self.__states, self.__initialState, - self.__finalState, self.__currentState) - - - # setters and getters - - def getName(self): - """getName(self)""" - return(self.__name) - - def setName(self, name): - """setName(self, string)""" - self.__name = name - - def getStates(self): - """getStates(self)""" - return(self.__states) - - def setStates(self, states): - """setStates(self, State[])""" - self.__states = states - - def addState(self, state): - """addState(state)""" - self.__states.append(state) - - def getInitialState(self): - """getInitialState(self)""" - return(self.__initialState) - - def setInitialState(self, initialStateName): - """setInitialState(self, string)""" - self.__initialState = self.getStateNamed(initialStateName) - - def getFinalState(self): - """getFinalState(self)""" - return(self.__finalState) - - def setFinalState(self, finalStateName): - """setFinalState(self, string)""" - self.__finalState = self.getStateNamed(finalStateName) - - def requestFinalState(self): - self.request(self.__finalState.getName()) - - def getCurrentState(self): - """getCurrentState(self)""" - return(self.__currentState) - - - # lookup funcs - - def getStateNamed(self, stateName): - """getStateNamed(self, string) - Return the state with given name if found, issue warning otherwise""" - for state in self.__states: - if (state.getName() == stateName): - return state - FSM.notify.warning("[%s] : getStateNamed: %s, no such state" % - (self.__name, str(stateName))) - - - # basic FSM functionality - - def __exitCurrent(self, argList): - """__exitCurrent(self) - Exit the current state""" - assert(self.__internalStateInFlux) - if FSM.notify.getDebug(): - FSM.notify.debug("[%s]: exiting %s" % (self.__name, - self.__currentState.getName())) - self.__currentState.exit(argList) - # Only send the state change event if we are inspecting it - # If this event turns out to be generally useful, we can - # turn it on all the time, but for now nobody else is using it - if self.inspecting: - messenger.send(self.getName() + '_' + - self.__currentState.getName() + '_exited') - self.__currentState = None - - def __enter(self, aState, argList=[]): - """__enter(self, State) - Enter a given state, if it exists""" - assert(self.__internalStateInFlux) - if (aState in self.__states): - if FSM.notify.getDebug(): - FSM.notify.debug("[%s]: entering %s" % (self.__name, - aState.getName())) - self.__currentState = aState - # Only send the state change event if we are inspecting it - # If this event turns out to be generally useful, we can - # turn it on all the time, but for now nobody else is using it - if self.inspecting: - messenger.send(self.getName() + '_' + - aState.getName() + '_entered') - - # Once we begin entering the new state, we're allow to - # recursively request a transition to another state. - # Indicate this by marking our internal state no longer in - # flux. - self.__internalStateInFlux = 0 - aState.enter(argList) - else: - # notify.error is going to raise an exception; reset the - # flux flag first - self.__internalStateInFlux = 0 - FSM.notify.error("[%s]: enter: no such state" % (self.__name)) - - def __transition(self, aState, enterArgList=[], exitArgList=[]): - """__transition(self, State, enterArgList, exitArgList) - Exit currentState and enter given one""" - assert(not self.__internalStateInFlux) - self.__internalStateInFlux = 1 - self.__exitCurrent(exitArgList) - self.__enter(aState, enterArgList) - assert(not self.__internalStateInFlux) - - def request(self, aStateName, enterArgList=[], exitArgList=[], - force=0): - """request(self, string) - Attempt transition from currentState to given one. - Return true is transition exists to given state, - false otherwise. - """ - - # If you trigger this assertion failure, you must have - # recursively requested a state transition from within the - # exitState() function for the previous state. This is not - # supported because we're not fully transitioned into the new - # state yet. - assert(not self.__internalStateInFlux) - - if not self.__currentState: - # Make this a warning for now - FSM.notify.warning("[%s]: request: never entered initial state" % - (self.__name)) - self.__currentState = self.__initialState - - if isinstance(aStateName, types.StringType): - aState = self.getStateNamed(aStateName) - else: - # Allow the caller to pass in a state in itself, not just - # the name of a state. - aState = aStateName - aStateName = aState.getName() - - if aState == None: - FSM.notify.error("[%s]: request: %s, no such state" % - (self.__name, aStateName)) - - # is the transition defined? if it isn't, should we allow it? - transitionDefined = self.__currentState.isTransitionDefined(aStateName) - transitionAllowed = transitionDefined - - if self.onUndefTransition == FSM.ALLOW: - transitionAllowed = 1 - if not transitionDefined: - # the transition is not defined, but we're going to do it - # anyway. print a warning. - FSM.notify.warning( - "[%s]: performing undefined transition from %s to %s" % - (self.__name, - self.__currentState.getName(), - aStateName)) - - if transitionAllowed or force: - self.__transition(aState, - enterArgList, - exitArgList) - return 1 - # We can implicitly always transition to our final state. - elif (aStateName == self.__finalState.getName()): - if (self.__currentState == self.__finalState): - # Do not do the transition if we are already in the final state - if FSM.notify.getDebug(): - FSM.notify.debug("[%s]: already in final state: %s" % - (self.__name, aStateName)) - return 1 - else: - # Force a transition to allow for cleanup - if FSM.notify.getDebug(): - FSM.notify.debug("[%s]: implicit transition to final state: %s" % - (self.__name, aStateName)) - self.__transition(aState, - enterArgList, - exitArgList) - return 1 - # are we already in this state? - elif (aStateName == self.__currentState.getName()): - if FSM.notify.getDebug(): - FSM.notify.debug("[%s]: already in state %s and no self transition" % - (self.__name, aStateName)) - return 0 - else: - msg = ("[%s]: no transition exists from %s to %s" % - (self.__name, - self.__currentState.getName(), - aStateName)) - if self.onUndefTransition == FSM.ERROR: - FSM.notify.error(msg) - elif self.onUndefTransition == FSM.DISALLOW_VERBOSE: - FSM.notify.warning(msg) - return 0 - - - def forceTransition(self, aStateName, enterArgList=[], exitArgList=[]): - """ force a transition -- for debugging ONLY """ - self.request(aStateName, enterArgList, exitArgList, force=1) - - def conditional_request(self, aStateName, enterArgList=[], exitArgList=[]): - """request(self, string) - 'if this transition is defined, do it' - Attempt transition from currentState to given one, if it exists. - Return true if transition exists to given state, - false otherwise. It is NOT an error/warning to attempt a cond_request - if the transition doesn't exist. This lets people be sloppy about - FSM transitions, letting the same fn be used for different states - that may not have the same out transitions. - """ - - assert(not self.__internalStateInFlux) - if not self.__currentState: - # Make this a warning for now - FSM.notify.warning("[%s]: request: never entered initial state" % - (self.__name)) - self.__currentState = self.__initialState - - if isinstance(aStateName, types.StringType): - aState = self.getStateNamed(aStateName) - else: - # Allow the caller to pass in a state in itself, not just - # the name of a state. - aState = aStateName - aStateName = aState.getName() - - if aState == None: - FSM.notify.error("[%s]: request: %s, no such state" % - (self.__name, aStateName)) - - transitionDefined = ( - self.__currentState.isTransitionDefined(aStateName) or - aStateName in [self.__currentState.getName(), - self.__finalState.getName()] - ) - - if transitionDefined: - return self.request(aStateName, enterArgList, exitArgList) - else: - FSM.notify.debug("[%s]: condition_request: %s, transition doesnt exist" % - (self.__name, aStateName)) - return 0 - - def view(self): - import FSMInspector - FSMInspector.FSMInspector(self) - - - - - - - - - - diff --git a/direct/src/fsm/FourState.py b/direct/src/fsm/FourState.py index b8affd94ff..e340c4fb45 100755 --- a/direct/src/fsm/FourState.py +++ b/direct/src/fsm/FourState.py @@ -2,14 +2,14 @@ import DirectNotifyGlobal #import DistributedObject -import FSM +import ClassicFSM import State import Task class FourState: """ - Generic four state FSM base class. + Generic four state ClassicFSM base class. This is a mix-in class that expects that your derived class is a DistributedObject. @@ -34,7 +34,7 @@ class FourState: +------+ There is a fifth off state, but that is an implementation - detail (and that's why it's not called a five state FSM). + detail (and that's why it's not called a five state ClassicFSM). I found that this pattern repeated in several things I was working on, so this base class was created. @@ -120,7 +120,7 @@ class FourState: [names[1]]), } self.stateIndex = 0 - self.fsm = FSM.FSM('FourState', + self.fsm = ClassicFSM.ClassicFSM('FourState', self.states.values(), # Initial State names[0], diff --git a/direct/src/fsm/FourStateAI.py b/direct/src/fsm/FourStateAI.py index e136bf9872..19e45b7113 100755 --- a/direct/src/fsm/FourStateAI.py +++ b/direct/src/fsm/FourStateAI.py @@ -2,14 +2,14 @@ import DirectNotifyGlobal #import DistributedObjectAI -import FSM +import ClassicFSM import State import Task class FourStateAI: """ - Generic four state FSM base class. + Generic four state ClassicFSM base class. This is a mix-in class that expects that your derived class is a DistributedObjectAI. @@ -34,7 +34,7 @@ class FourStateAI: +------+ There is a fifth off state, but that is an implementation - detail (and that's why it's not called a five state FSM). + detail (and that's why it's not called a five state ClassicFSM). I found that this pattern repeated in several things I was working on, so this base class was created. @@ -58,7 +58,7 @@ class FourStateAI: durations is a list of durations in seconds or None values. The list of duration values should be the same length as the list of state names and the lists correspond. - For each state, after n seconds, the FSM will move to + For each state, after n seconds, the ClassicFSM will move to the next state. That does not happen for any duration values of None. @@ -125,7 +125,7 @@ class FourStateAI: self.exitState4, [names[1]]), } - self.fsm = FSM.FSM('FourState', + self.fsm = ClassicFSM.ClassicFSM('FourState', self.states.values(), # Initial State names[0], diff --git a/direct/src/fsm/SampleFSM.py b/direct/src/fsm/SampleFSM.py new file mode 100644 index 0000000000..a8c0538012 --- /dev/null +++ b/direct/src/fsm/SampleFSM.py @@ -0,0 +1,193 @@ +import FSM +from PandaModules import * +import Task +import string + + +class ClassicStyle(FSM.FSM): + + def __init__(self, name): + FSM.FSM.__init__(self, name) + + self.defaultTransitions = { + 'Red' : ['Green'], + 'Yellow' : ['Red'], + 'Green' : ['Yellow'], + } + + def enterRed(self, oldState, newState): + print "enterRed(self, '%s', '%s')" % (oldState, newState) + + def exitRed(self, oldState, newState): + print "exitRed(self, '%s', '%s')" % (oldState, newState) + + def enterYellow(self, oldState, newState): + print "enterYellow(self, '%s', '%s')" % (oldState, newState) + + def exitYellow(self, oldState, newState): + print "exitYellow(self, '%s', '%s')" % (oldState, newState) + + def enterGreen(self, oldState, newState): + print "enterGreen(self, '%s', '%s')" % (oldState, newState) + + def exitGreen(self, oldState, newState): + print "exitGreen(self, '%s', '%s')" % (oldState, newState) + + +class NewStyle(FSM.FSM): + + def enterRed(self, oldState, newState): + print "enterRed(self, '%s', '%s')" % (oldState, newState) + + def filterRed(self, request, args): + print "filterRed(self, '%s', %s)" % (request, args) + if request == 'advance': + return 'Green' + return self.defaultFilter(request, args) + + def exitRed(self, oldState, newState): + print "exitRed(self, '%s', '%s')" % (oldState, newState) + + def enterYellow(self, oldState, newState): + print "enterYellow(self, '%s', '%s')" % (oldState, newState) + + def filterYellow(self, request, args): + print "filterYellow(self, '%s', %s)" % (request, args) + if request == 'advance': + return 'Red' + return self.defaultFilter(request, args) + + def exitYellow(self, oldState, newState): + print "exitYellow(self, '%s', '%s')" % (oldState, newState) + + def enterGreen(self, oldState, newState): + print "enterGreen(self, '%s', '%s')" % (oldState, newState) + + def filterGreen(self, request, args): + print "filterGreen(self, '%s', %s)" % (request, args) + if request == 'advance': + return 'Yellow' + return self.defaultFilter(request, args) + + def exitGreen(self, oldState, newState): + print "exitGreen(self, '%s', '%s')" % (oldState, newState) + + +class ToonEyes(FSM.FSM): + def __init__(self): + FSM.FSM.__init__(self, 'eyes') + + self.__unblinkName = "unblink" + + # Eyes are initially open. + self.request('Open') + + def defaultFilter(self, request, args): + # The default filter accepts any direct state request (these + # start with a capital letter). + if request[0] in string.uppercase: + return request + + # Unexpected command requests are quietly ignored. + return None + + def enterOpen(self, oldState, newState): + print "swap in eyes open model" + + def filterOpen(self, request, args): + if request == 'blink': + taskMgr.remove(self.__unblinkName) + taskMgr.doMethodLater(0.125, self.__unblink, self.__unblinkName) + return 'Closed' + return self.defaultFilter(request, args) + + def __unblink(self, task): + self.request('unblink') + return Task.done + + def enterClosed(self, oldState, newState): + print "swap in eyes closed model" + + def filterClosed(self, request, args): + if request == 'unblink': + return 'Open' + return self.defaultFilter(request, args) + + def enterSurprised(self, oldState, newState): + print "swap in eyes surprised model" + + def enterOff(self, oldState, newState): + taskMgr.remove(self.__unblinkName) + + +#### +#### Example of using ClassicStyle: +## +## >>> import SampleFSM +## >>> foo = SampleFSM.ClassicStyle('foo') +## >>> foo.request('Red') +## enterRed(self, 'Off', 'Red') +## 'Red' +## >>> foo.request('Yellow') +## Traceback (most recent call last): +## File "", line 1, in ? +## File "/home/drose/player/direct/src/fsm/FSM.py", line 168, in request +## result = func(request, args) +## File "/home/drose/player/direct/src/fsm/FSM.py", line 210, in defaultFilter +## self.notify.error("%s rejecting request %s from state %s." % (self.name, request, self.state)) +## File "/home/drose/player/direct/src/directnotify/Notifier.py", line 99, in error +## raise exception(errorString) +## StandardError: foo rejecting request Yellow from state Red. +## >>> foo.request('Green') +## exitRed(self, 'Red', 'Green') +## enterGreen(self, 'Red', 'Green') +## 'Green' +## >>> + +#### +#### Example of using NewStyle: +## +## >>> import SampleFSM +## >>> foo = SampleFSM.NewStyle('foo') +## >>> foo.request('Red') +## enterRed(self, 'Off', 'Red') +## 'Red' +## >>> foo.request('advance') +## filterRed(self, 'advance', ()) +## exitRed(self, 'Red', 'Green') +## enterGreen(self, 'Red', 'Green') +## 'Green' +## >>> foo.request('advance') +## filterGreen(self, 'advance', ()) +## exitGreen(self, 'Green', 'Yellow') +## enterYellow(self, 'Green', 'Yellow') +## 'Yellow' +## >>> foo.request('advance') +## filterYellow(self, 'advance', ()) +## exitYellow(self, 'Yellow', 'Red') +## enterRed(self, 'Yellow', 'Red') +## 'Red' +## >>> foo.request('advance') +## filterRed(self, 'advance', ()) +## exitRed(self, 'Red', 'Green') +## enterGreen(self, 'Red', 'Green') +## 'Green' +## >>> + +#### +#### Example of using ToonEyes: +## +## >>> from ShowBaseGlobal import * +## >>> import SampleFSM +## >>> eyes = SampleFSM.ToonEyes() +## swap in eyes open model +## >>> eyes.request('blink') +## swap in eyes closed model +## 'Closed' +## >>> run() +## swap in eyes open model +## >>> eyes.request('Surprised') +## swap in eyes surprised model +## 'Surprised' +## >>> eyes.request('blink') +## >>> diff --git a/direct/src/fsm/State.py b/direct/src/fsm/State.py index f94008b2fa..e1d210ae09 100644 --- a/direct/src/fsm/State.py +++ b/direct/src/fsm/State.py @@ -5,7 +5,7 @@ from DirectObject import * import types # This gets set by a dconfig variable in ShowBase.py -# We cannot put a dconfig in here because FSM is not +# We cannot put a dconfig in here because ClassicFSM is not # dependent on Panda FsmRedefine = 0 @@ -193,20 +193,20 @@ class State(DirectObject): return(self.__FSMList) def setChildren(self, FSMList): - """setChildren(self, FSM[]) + """setChildren(self, ClassicFSM[]) Set the children to given list of FSMs""" self.__FSMList = FSMList - def addChild(self, FSM): - """addChild(self, FSM) - Add the given FSM to list of child FSMs""" - self.__FSMList.append(FSM) + def addChild(self, ClassicFSM): + """addChild(self, ClassicFSM) + Add the given ClassicFSM to list of child FSMs""" + self.__FSMList.append(ClassicFSM) - def removeChild(self, FSM): - """removeChild(self, FSM) - Remove the given FSM from list of child FSMs""" - if FSM in self.__FSMList: - self.__FSMList.remove(FSM) + def removeChild(self, ClassicFSM): + """removeChild(self, ClassicFSM) + Remove the given ClassicFSM from list of child FSMs""" + if ClassicFSM in self.__FSMList: + self.__FSMList.remove(ClassicFSM) def hasChildren(self): """hasChildren(self) diff --git a/direct/src/fsm/StateData.py b/direct/src/fsm/StateData.py index f2923049de..6eaf0da7e0 100644 --- a/direct/src/fsm/StateData.py +++ b/direct/src/fsm/StateData.py @@ -8,7 +8,7 @@ import DirectNotifyGlobal class StateData(DirectObject): """ A StateData is a base class for a single state within a Finite - State Machine (FSM). + State Machine (ClassicFSM). """ notify = DirectNotifyGlobal.directNotify.newCategory('StateData') diff --git a/direct/src/level/CutScene.py b/direct/src/level/CutScene.py index d022978ef5..0e4a34e56f 100755 --- a/direct/src/level/CutScene.py +++ b/direct/src/level/CutScene.py @@ -12,7 +12,7 @@ from ClockDelta import * import ToontownGlobals import DirectNotifyGlobal -import FSM +import ClassicFSM #import DistributedInteractiveEntity import DelayDelete import Localizer diff --git a/direct/src/level/DistributedInteractiveEntity.py b/direct/src/level/DistributedInteractiveEntity.py index c254313ebb..3bbee44085 100644 --- a/direct/src/level/DistributedInteractiveEntity.py +++ b/direct/src/level/DistributedInteractiveEntity.py @@ -5,7 +5,7 @@ from ShowBaseGlobal import * from ClockDelta import * import DirectNotifyGlobal -import FSM +import ClassicFSM import DistributedEntity class DistributedInteractiveEntity(DistributedEntity.DistributedEntity): @@ -22,7 +22,7 @@ class DistributedInteractiveEntity(DistributedEntity.DistributedEntity): DistributedEntity.DistributedEntity.__init__(self, cr) assert(self.debugPrint("DistributedInteractiveEntity()")) - self.fsm = FSM.FSM('DistributedInteractiveEntity', + self.fsm = ClassicFSM.ClassicFSM('DistributedInteractiveEntity', [State.State('off', self.enterOff, self.exitOff, diff --git a/direct/src/level/DistributedInteractiveEntityAI.py b/direct/src/level/DistributedInteractiveEntityAI.py index 66a7dc7dc6..4db7916444 100644 --- a/direct/src/level/DistributedInteractiveEntityAI.py +++ b/direct/src/level/DistributedInteractiveEntityAI.py @@ -7,7 +7,7 @@ from AIBaseGlobal import * from ClockDelta import * import DirectNotifyGlobal -import FSM +import ClassicFSM import DistributedEntityAI import State @@ -30,7 +30,7 @@ class DistributedInteractiveEntityAI(DistributedEntityAI.DistributedEntityAI): assert(self.debugPrint( "DistributedInteractiveEntityAI(entId=%s)" %(entId))) - self.fsm = FSM.FSM('DistributedInteractiveEntityAI', + self.fsm = ClassicFSM.ClassicFSM('DistributedInteractiveEntityAI', [State.State('off', self.enterOff, self.exitOff, diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index 4ce579c682..bf59a1b62f 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -21,7 +21,7 @@ import math import sys import Loader import time -import FSM +import ClassicFSM import State import DirectObject import SfxPlayer diff --git a/direct/src/tkpanels/FSMInspector.py b/direct/src/tkpanels/FSMInspector.py index 990add6449..1b8075e536 100644 --- a/direct/src/tkpanels/FSMInspector.py +++ b/direct/src/tkpanels/FSMInspector.py @@ -10,7 +10,7 @@ DELTA = (5.0 / 360.) * 2.0 * math.pi class FSMInspector(AppShell): # Override class variables - appname = 'FSM Inspector' + appname = 'ClassicFSM Inspector' frameWidth = 400 frameHeight = 450 usecommandarea = 0 @@ -44,23 +44,23 @@ class FSMInspector(AppShell): interior = self.interior() menuBar = self.menuBar - # FSM Menu - menuBar.addmenu('FSM', 'FSM Operations') - menuBar.addmenuitem('FSM', 'command', + # ClassicFSM Menu + menuBar.addmenu('ClassicFSM', 'ClassicFSM Operations') + menuBar.addmenuitem('ClassicFSM', 'command', 'Input grid spacing', label = 'Grid spacing...', command = self.popupGridDialog) # Create the checkbutton variable self._fGridSnap = IntVar() self._fGridSnap.set(1) - menuBar.addmenuitem('FSM', 'checkbutton', + menuBar.addmenuitem('ClassicFSM', 'checkbutton', 'Enable/disable grid', label = 'Snap to grid', variable = self._fGridSnap, command = self.toggleGridSnap) - menuBar.addmenuitem('FSM', 'command', - 'Print out FSM layout', - label = 'Print FSM layout', + menuBar.addmenuitem('ClassicFSM', 'command', + 'Print out ClassicFSM layout', + label = 'Print ClassicFSM layout', command = self.printLayout) # States Menu @@ -238,7 +238,7 @@ class FSMInspector(AppShell): self.stateInspectorDict[key].setGridSize(size) def popupGridDialog(self): - spacing = askstring('FSM Grid Spacing', 'Grid Spacing:') + spacing = askstring('ClassicFSM Grid Spacing', 'Grid Spacing:') if spacing: self.setGridSize(spacing) self._gridSize = spacing @@ -253,7 +253,7 @@ class FSMInspector(AppShell): dict = self.stateInspectorDict keys = dict.keys() keys.sort - print "FSM.FSM('%s', [" % self.name + print "ClassicFSM.ClassicFSM('%s', [" % self.name for key in keys[:-1]: si = dict[key] center = si.center() @@ -282,7 +282,7 @@ class FSMInspector(AppShell): self.balloon.configure(state = 'none') def onDestroy(self, event): - """ Called on FSM Panel shutdown """ + """ Called on ClassicFSM Panel shutdown """ self.fsm.inspecting = 0 for si in self.stateInspectorDict.values(): self.ignore(self.name + '_' + si.getName() + '_entered') @@ -454,7 +454,7 @@ want-tk #t from ShowBaseGlobal import * -import FSM +import ClassicFSM import State def enterState(): @@ -463,7 +463,7 @@ def enterState(): def exitState(): print 'exitState' -fsm = FSM.FSM('stopLight', +fsm = ClassicFSM.ClassicFSM('stopLight', [ State.State('red', enterState, exitState, ['green']), State.State('yellow', enterState, exitState, ['red']), State.State('green', enterState, exitState, ['yellow']) ], @@ -476,7 +476,7 @@ inspector = FSMInspector.FSMInspector(fsm, title = fsm.getName()) # Note, the inspectorPos argument is optional, the inspector will # automagically position states on startup -fsm = FSM.FSM('stopLight', [ +fsm = ClassicFSM.ClassicFSM('stopLight', [ State.State('yellow', enterState, exitState, @@ -510,8 +510,8 @@ Features: - Middle mouse button will grab the canvas and slide things around if your state machine is bigger than the viewing area - There are some self explanatory menu options up at the top, the most - useful being: "print FSM layout" which will print out python code - which will create an FSM augmented with layout information for the + useful being: "print ClassicFSM layout" which will print out python code + which will create an ClassicFSM augmented with layout information for the viewer so everything shows up in the same place the next time you inspect the state machine diff --git a/direct/src/tkpanels/Inspector.py b/direct/src/tkpanels/Inspector.py index 4fa6c54c87..5d97abf6dc 100644 --- a/direct/src/tkpanels/Inspector.py +++ b/direct/src/tkpanels/Inspector.py @@ -408,7 +408,7 @@ class InspectorWindow: part = self.topInspector().partNumber(partNumber) print part from PandaModules import NodePath - import FSM + import ClassicFSM popupMenu = None if isinstance(part, NodePath): popupMenu = self.createPopupMenu( @@ -416,11 +416,11 @@ class InspectorWindow: [('Explore', NodePath.explore), ('Place', NodePath.place), ('Set Color', NodePath.rgbPanel)]) - elif isinstance(part, FSM.FSM): + elif isinstance(part, ClassicFSM.ClassicFSM): import FSMInspector popupMenu = self.createPopupMenu( part, - [('Inspect FSM', FSMInspector.FSMInspector)]) + [('Inspect ClassicFSM', FSMInspector.FSMInspector)]) print popupMenu if popupMenu: popupMenu.post(event.widget.winfo_pointerx(),