2009-12-27 22:16:41 +00:00
-----------------------------------------------------------------------------------
-- MindState.lua
-- Enrique Garc<72> a ( enrique.garcia.cota [AT] gmail [DOT] com ) - 19 Oct 2009
-- Based on Unrealscript's stateful objects
-----------------------------------------------------------------------------------
require ' MiddleClass.lua '
--[[ StatefulObject declaration
* Stateful classes have a list of states ( accesible through class.states ) .
* When a method is invoked on an instance of such classes , it is first looked up on the class current state ( accesible through class.currentState )
* If a method is not found on the current state , or if current state is nil , the method is looked up on the class itself
* It is possible to change states by doing class : gotoState ( stateName )
] ]
StatefulObject = class ( ' StatefulObject ' )
2010-02-14 23:37:10 +00:00
StatefulObject.states = { } -- the root state list
local private = setmetatable ( { } , { __mode = " k " } ) -- weak table storing private references
2009-12-27 22:16:41 +00:00
-- Instance methods
--[[ constructor
If your states need initialization , they can receive parameters via the initParameters parameter
initParameters is a table with parameters used for initializing the states . These are needed mostly if
your states have a custom superclass that needs parameters on their initialize ( ) function .
] ]
function StatefulObject : initialize ( initParameters )
2010-02-07 15:52:00 +00:00
super.initialize ( self )
2009-12-27 22:16:41 +00:00
initParameters = initParameters or { } --initialize to empty table if nil
2010-02-14 23:37:10 +00:00
self.states = { }
private [ self ] = {
stateStack = { }
}
2009-12-27 22:16:41 +00:00
for stateName , stateClass in pairs ( self.class . states ) do
local state = stateClass : new ( unpack ( initParameters [ stateName ] or { } ) )
state.name = stateName
self.states [ stateName ] = state
end
end
--[[ Changes the current state.
If the current state has a method called onExitState , it will be called , with the instance as a parameter .
If the " next " state exists and has a method called onExitState , it will be called , with the instance as a parameter .
use gotoState ( nil ) for setting states to nothing
2010-02-14 23:37:10 +00:00
This method invokes the exitState and enterState functions if they exist on the current state
Second parameter is optional . If true , the stack will be conserved . Otherwise , it will be popped .
2009-12-27 22:16:41 +00:00
] ]
2010-02-14 23:37:10 +00:00
function StatefulObject : gotoState ( newStateName , keepStack )
2010-02-07 15:52:00 +00:00
assert ( self.states ~= nil , " Attribute 'states' not detected. check that you called instance:gotoState and not instance.gotoState, and that you invoked super.initialize(self) in the constructor. " )
2010-02-14 23:37:10 +00:00
local prevState = self : getCurrentState ( )
-- If we're trying to go to a state in which we already are, return (do nothing)
if ( prevState ~= nil and prevState.name == newStateName ) then return end
2010-02-06 19:50:17 +00:00
local nextState
2010-02-14 23:37:10 +00:00
if ( newStateName ~= nil ) then
nextState = self.states [ newStateName ]
assert ( nextState ~= nil , " State ' " .. newStateName .. " ' not found " )
2010-02-06 19:50:17 +00:00
end
2009-12-27 22:16:41 +00:00
2010-02-14 23:37:10 +00:00
-- Invoke exitState on the previous state
if ( prevState ~= nil and type ( prevState.exitState ) == " function " ) then prevState.exitState ( self , newStateName ) end
2009-12-27 22:16:41 +00:00
2010-02-14 23:37:10 +00:00
-- Empty the stack unless keepStack is true.
if ( keepStack ~= true ) then self : popAllStates ( ) end
-- replace the top of the stack with the new state
local stack = private [ self ] . stateStack
stack [ math.max ( # stack , 1 ) ] = nextState
2009-12-27 22:16:41 +00:00
2010-02-14 23:37:10 +00:00
-- Invoke enterState on the new state. 2nd parameter is the name of the previous state, or nil
if ( nextState ~= nil and type ( nextState.enterState ) == " function " ) then
nextState.enterState ( self , prevState ~= nil and prevState.name or nil )
2009-12-27 22:16:41 +00:00
end
2010-02-14 23:37:10 +00:00
end
function StatefulObject : pushState ( newStateName )
assert ( type ( newState ) == ' string ' , " newStateName must be a string. " )
assert ( self.states ~= nil , " Attribute 'states' not detected. check that you called instance:pushState and not instance.pushState, and that you invoked super.initialize(self) in the constructor. " )
local nextState = self.states [ newStateName ]
assert ( nextState ~= nil , " State ' " .. newStateName .. " ' not found " )
-- If we attempt to push a state and the state is already on return (do nothing)
local stack = private [ self ] . stateStack
for _ , state in ipairs ( stack ) do
if ( state.name == newStateName ) then return end
end
-- Invoke pausedState on the previous state
local prevState = self : getCurrentState ( )
if ( prevState ~= nil and type ( prevState.pausedState ) == " function " ) then prevState.pausedState ( self ) end
-- Do the push
table.insert ( stack , nextState )
-- Invoke pushState on the next state
if ( type ( nextState.pushedState ) == " function " ) then nextState.pushedState ( self ) end
return nextState
end
-- If a state name is given, it will attempt to remove it from the stack. If not found on the stack it will do nothing.
-- If no state name is give, this pops the top state from the stack, if any. Otherwise it does nothing.
-- Callbacks will be called when needed.
function StatefulObject : popState ( stateName )
assert ( self.states ~= nil , " Attribute 'states' not detected. check that you called instance:popState and not instance.popState, and that you invoked super.initialize(self) in the constructor. " )
-- Invoke poppedState on the previous state
local prevState = self : getCurrentState ( )
if ( prevState ~= nil and type ( prevState.poppedState ) == " function " ) then prevState.poppedState ( self ) end
-- Do the pop
local stack = private [ self ] . stateStack
table.remove ( stack , # stack )
-- Invoke continuedState on the new state
local newState = self : getCurrentState ( )
if ( newState ~= nil and type ( newState.continuedState ) == " function " ) then newState.continuedState ( self ) end
return newState
end
function StatefulObject : popAllStates ( )
local state = self : popState ( )
while ( state ~= nil ) do state = self : popState ( ) end
end
function StatefulObject : getCurrentState ( )
local stack = private [ self ] . stateStack
if # stack == 0 then return nil end
return ( stack [ # stack ] )
2009-12-27 22:16:41 +00:00
end
2010-02-14 23:37:10 +00:00
--[[
Returns true if the object is in the state named ' stateName '
If second ( optional ) parameter is true , this method returns true if the state is on the stack instead
] ]
function StatefulObject : inState ( stateName , testStateStack )
local stack = private [ self ] . stateStack
if ( testStateStack == true ) then
for _ , state in ipairs ( stack ) do
if ( state.name == stateName ) then return true end
end
else --testStateStack==false
local state = stack [ # stack ]
if ( state ~= nil and state.name == stateName ) then return true end
end
return false
end
2009-12-27 22:16:41 +00:00
-- Class methods
--[[ Adds a new state to the "states" class member.
superState is optional . If nil , Object will be the parent class of the new state
returns the newly created state
] ]
function StatefulObject : addState ( stateName , superState )
assert ( subclassOf ( StatefulObject , self ) , " Use class:addState instead of class.addState " )
assert ( self.states [ stateName ] == nil , " The class " .. self.name .. " already has a state called ' " .. stateName )
assert ( type ( stateName ) == " string " , " stateName must be a string " )
-- states are just regular classes. If superState is nil, this uses Object as superClass
local state = class ( stateName , superState )
self.states [ stateName ] = state
return state
end
2010-02-14 23:37:10 +00:00
-- These methods will not be overriden by the states.
local ignoredMethods = {
states = 1 , initialize = 1 ,
gotoState = 1 , pushState = 1 , popState = 1 , popAllStates = 1 , getCurrentState = 1 , inState = 1 ,
enterState = 1 , exitState = 1 , pushedState = 1 , poppedState = 1 , pausedState = 1 , continuedState = 1 ,
addState = 1 , subclass = 1 , includes = 1
}
2010-01-26 23:44:44 +00:00
local prevSubclass = StatefulObject.subclass
--[[ creates a stateful subclass
Subclasses inherit all the states of their superclases , in a special way :
If class A has a state called Sleeping and B = A.subClass ( ' B ' ) , then B.states . Sleeping is a subclass of A.states . Sleeping
returns the newly created stateful class
] ]
function StatefulObject : subclass ( name )
--assert(subclassOf(StatefulObject, self), "Use class:subclass instead of class.subclass")
local theClass = prevSubclass ( self , name ) --for now, theClass is just a regular subclass
--the states of the subclass are subclasses of the superclass' states
theClass.states = { }
for stateName , state in pairs ( self.states ) do
theClass : addState ( stateName , state )
end
2009-12-27 22:16:41 +00:00
2010-01-26 23:44:44 +00:00
--make sure that the currentState is used on the method lookup function before looking on the class dict
local classDict = theClass.__classDict
2010-02-14 23:37:10 +00:00
classDict.__index = function ( instance , methodName )
-- If the method isn't on the 'ignoredMethods' list, look through the stack to see if it is defined
if ( ignoredMethods [ methodName ] ~= 1 ) then
local stack = private [ instance ] . stateStack
local method
for i = # stack , 1 , - 1 do -- reversal loop
method = stack [ i ] [ methodName ]
if ( method ~= nil ) then return method end
end
2010-01-26 23:44:44 +00:00
end
2010-02-14 23:37:10 +00:00
--if ignored or not found, look on the class itself
return classDict [ methodName ]
2009-12-27 22:16:41 +00:00
end
2010-01-26 23:44:44 +00:00
return theClass
end
2009-12-27 22:16:41 +00:00
--[[ Include override for stateful classes.
This is exactly like MiddleClass ' include function, except that it module has a property called "states"
then each member of that module.states is included on the StatefulObject class .
If module.states has a state that doesn ' t exist on StatefulObject, a new state will be created.
] ]
2010-02-14 23:37:10 +00:00
function StatefulObject : includes ( module , ... )
2009-12-27 22:16:41 +00:00
assert ( subclassOf ( StatefulObject , self ) , " Use class:includes instead of class.includes " )
for methodName , method in pairs ( module ) do
if methodName ~= " included " and methodName ~= " states " then
self [ methodName ] = method
end
end
2010-02-14 23:37:10 +00:00
if type ( module.included ) == " function " then module.included ( self , ... ) end
2009-12-27 22:16:41 +00:00
if type ( module.states ) == " table " then
for stateName , moduleState in pairs ( module.states ) do
local state = self.states [ stateName ]
if ( state == nil ) then state = theClass : addState ( stateName ) end
2010-02-14 23:37:10 +00:00
state : includes ( moduleState , ... )
2009-12-27 22:16:41 +00:00
end
end
end