removed getterSetters from main MiddleClass (use a mixin instead). Added destroy. Fixed bug with destroying stateful objects

This commit is contained in:
kikito 2010-07-14 15:19:17 +02:00
parent bf24742c9c
commit 64f2d89281
2 changed files with 205 additions and 194 deletions

49
MiddleClass.lua Normal file → Executable file
View File

@ -7,9 +7,9 @@
local _classes = setmetatable({}, {__mode = "k"}) -- weak table storing references to all declared _classes
-- The 'Object' class
Object = { name = "Object" }
_classes[Object]=Object -- adds Object to the list of _classes
Object = { name = "Object" }
_classes[Object]=Object -- adds Object to the list of _classes
-- creates a new instance
Object.new = function(theClass, ...)
@ -44,8 +44,8 @@ Object.subclass = function(theClass, name)
-- FIXME add support for __index method here
__newindex = function(_, methodName, method) -- when adding new methods, include a "super" function
if type(method) == 'function' then
local fenv = getfenv(method)
local newenv = setmetatable( {super = theClass.__classDict}, {__index = fenv, __newindex = fenv} )
local fenv = getfenv(method)
local newenv = setmetatable( {super = theClass.__classDict}, {__index = fenv, __newindex = fenv} )
setfenv( method, newenv )
end
rawset(classDict, methodName, method)
@ -58,6 +58,8 @@ Object.subclass = function(theClass, name)
_classes[theSubclass]=theSubclass --registers the new class on the list of _classes
theClass:subclassed(theSubclass) -- hook method. By default it does nothing
return theSubclass
end
@ -72,49 +74,34 @@ Object.includes = function(theClass, module, ... )
if type(module.included)=="function" then module:included(theClass, ... ) end
end
-- root of initialize and __tostring methods
-- built-in methods
Object.__classDict = {
initialize = function(instance, ...) end, -- end of the initialize() call chain
__tostring = function(instance) return ("instance of ".. instance.class.name) end
initialize = function(instance, ...) end, -- empty method
destroy = function(instance) end, -- empty method
__tostring = function(instance) return ("instance of ".. instance.class.name) end,
subclassed = function(theClass, other) end -- empty method
}
-- This allows doing tostring(obj) and Object() instead of Object:new()
setmetatable(Object, { __index = Object.__classDict, __newindex = Object.__classDict,
__tostring = function() return ("class Object") end,
__call = Object.new
})
-- Getter/Setter related methods
function Object.getterFor(theClass, attr) return 'get' .. attr:gsub("^%l", string.upper) end
function Object.setterFor(theClass, attr) return 'set' .. attr:gsub("^%l", string.upper) end
function Object.getter(theClass, attributeName, defaultValue)
theClass[theClass:getterFor(attributeName)] = function(self)
if(self[attributeName]~=nil) then return self[attributeName] end
return defaultValue
end
end
function Object.setter(theClass, attributeName)
theClass[theClass:setterFor(attributeName)] = function(self, value) self[attributeName] = value end
end
function Object.getterSetter(theClass, attributeName, defaultValue)
theClass:getter(attributeName, defaultValue)
theClass:setter(attributeName)
end
-- Returns true if aClass is a subclass of other, false otherwise
function subclassOf(other, aClass)
function subclassOf(other, aClass)
if aClass == nil or other==nil then return false end
if aClass.superclass==nil then return false end -- aClass is Object, or a non-class
return aClass.superclass == other or subclassOf(other, aClass.superclass)
end
-- Returns true if obj is an instance of aClass (or one of its subclasses) false otherwise
function instanceOf(aClass, obj)
if obj==nil or _classes[aClass]==nil or _classes[obj.class]==nil then return false end
if obj.class==aClass then return true end
function instanceOf(aClass, obj)
if obj==nil or _classes[aClass]==nil or _classes[obj.class]==nil then return false end
if obj.class==aClass then return true end
return subclassOf(aClass, obj.class)
end
-- Creates a new class named 'name'. It uses baseClass as the parent (Object if none specified)
function class(name, baseClass)
baseClass = baseClass or Object

350
MindState.lua Normal file → Executable file
View File

@ -4,8 +4,8 @@
-- Based on Unrealscript's stateful objects
-----------------------------------------------------------------------------------
assert(Object~=nil and class~=nil, 'MiddleClass not detected. Please require it before using MindState')
assert(Object~=nil and class~=nil, 'MiddleClass not detected. Please require it before using MindState')
--[[ 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)
@ -14,39 +14,61 @@ assert(Object~=nil and class~=nil, 'MiddleClass not detected. Please require it
]]
StatefulObject = class('StatefulObject')
StatefulObject.states = {} -- the root state list
------------------------------------
-- PRIVATE ATTRIBUTES AND METHODS
------------------------------------
local _private = setmetatable({}, {__mode = "k"}) -- weak table storing private references
-- helper function used to call state callbacks (enterState, exitState, etc)
local _invokeCallback = function(self, state, callbackName, ... )
if state==nil then return end
local callback = state[callbackName]
if(type(callback)=='function') then callback(self, ...) end
end
local _getStack=function(self)
local stack = _private[self].stateStack
assert(stack~=nil, "Could not find the stack for the object. Make sure you invoked super.initialize(self) on the constructor.")
return stack
end
-- 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, isInState=1,
enterState=1, exitState=1, pushedState=1, poppedState=1, pausedState=1, continuedState=1,
addState=1, subclass=1, includes=1
}
StatefulObject.states = {} -- the root state list
------------------------------------
-- PRIVATE ATTRIBUTES AND METHODS
------------------------------------
local _private = setmetatable({}, {__mode = "k"}) -- weak table storing private references
-- helper function used to call state callbacks (enterState, exitState, etc)
local _invokeCallback = function(self, state, callbackName, ... )
if state==nil then return end
local callback = state[callbackName]
if(type(callback)=='function') then callback(self, ...) end
end
local _getStack=function(self)
local stack = _private[self].stateStack
assert(stack~=nil, "Could not find the stack for the object. Make sure you invoked super.initialize(self) on the constructor.")
return stack
end
-- 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, isInState=1,
enterState=1, exitState=1, pushedState=1, poppedState=1, pausedState=1, continuedState=1,
addState=1, subclass=1, includes=1, destroy=1
}
local _prevSubclass = StatefulObject.subclass -- previous way of creating subclasses (used to redefine subclass itself)
-- The State class; is the father of all State objects
local State = class('State', Object)
function State.subclass(theClass, name, theStatefulClass)
local theSubClass = Object.subclass(theClass, name)
local superDict = (theClass==State and theClass.__classDict or theStatefulClass.superclass.__classDict)
theSubClass.subclass = State.subclass
local mt = getmetatable(theSubClass)
mt.__newindex = function(_, methodName, method)
if type(method) == 'function' then
local fenv = getfenv(method)
local newenv = setmetatable( {super = superDict}, {__index = fenv, __newindex = fenv} )
setfenv( method, newenv )
end
rawset(theSubClass.__classDict, methodName, method)
end
return theSubClass
end
------------------------------------
-- INSTANCE METHODS
------------------------------------
------------------------------------
--[[ constructor
If your states need initialization, they can receive parameters via the initParameters parameter
@ -56,11 +78,11 @@ local _prevSubclass = StatefulObject.subclass -- previous way of creating subcla
function StatefulObject:initialize(initParameters)
super.initialize(self)
initParameters = initParameters or {} --initialize to empty table if nil
_private[self] = {
states = {},
stateStack = {}
}
_private[self] = {
states = {},
stateStack = {}
}
for stateName,stateClass in pairs(self.class.states) do
local state = stateClass:new(unpack(initParameters[stateName] or {}))
@ -73,157 +95,159 @@ end
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
This method invokes the exitState and enterState functions if they exist on the current state
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.
]]
function StatefulObject:gotoState(newStateName, keepStack)
assert(_private[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.")
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
local nextState
if(newStateName~=nil) then
nextState = _private[self].states[newStateName]
assert(nextState~=nil, "State '" .. newStateName .. "' not found")
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
local nextState
if(newStateName~=nil) then
nextState = _private[self].states[newStateName]
assert(nextState~=nil, "State '" .. newStateName .. "' not found")
end
-- Either empty completely the stack, or just call the exitstate callback on current state
if(keepStack~=true) then
self:popAllStates()
else
_invokeCallback(self, prevState, 'exitState', newStateName )
end
-- replace the top of the stack with the new state
local stack = _getStack(self)
-- Either empty completely the stack, or just call the exitstate callback on current state
if(keepStack~=true) then
self:popAllStates()
else
_invokeCallback(self, prevState, 'exitState', newStateName )
end
-- replace the top of the stack with the new state
local stack = _getStack(self)
stack[math.max(#stack,1)] = nextState
-- Invoke enterState on the new state. 2nd parameter is the name of the previous state, or nil
_invokeCallback(self, nextState, 'enterState', prevState~=nil and prevState.name or nil)
-- Invoke enterState on the new state. 2nd parameter is the name of the previous state, or nil
_invokeCallback(self, nextState, 'enterState', prevState~=nil and prevState.name or nil)
end
--[[ Changes the current state, by pushing a new state on the stack.
If the pushed state is already on the stack, this function does nothing.
Invokes 'pausedState' on the previous state, if existing
The new state is pushed on the top of the stack and then
Invokes 'pushedState' and 'enterState' on the new state, if existing
]]
function StatefulObject:pushState(newStateName)
assert(type(newStateName)=='string', "newStateName must be a string.")
end
--[[ Changes the current state, by pushing a new state on the stack.
If the pushed state is already on the stack, this function does nothing.
Invokes 'pausedState' on the previous state, if existing
The new state is pushed on the top of the stack and then
Invokes 'pushedState' and 'enterState' on the new state, if existing
]]
function StatefulObject:pushState(newStateName)
assert(type(newStateName)=='string', "newStateName must be a string.")
assert(_private[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 = _private[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 = _getStack(self)
local nextState = _private[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 = _getStack(self)
for _,state in ipairs(stack) do
if(state.name == newStateName) then return end
end
-- Invoke pausedState on the previous state
_invokeCallback(self, self:getCurrentState(), 'pausedState')
-- Do the push
table.insert(stack, nextState)
-- Invoke pushedState & enterState on the next state
_invokeCallback(self, nextState, 'pushedState')
_invokeCallback(self, nextState, 'enterState')
return nextState
end
--[[ Removes a state from the state stack
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(_private[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 exitstate & poppedState on the state being popped out
local prevState = self:getCurrentState()
_invokeCallback(self, prevState, 'exitState')
_invokeCallback(self, prevState, 'poppedState')
-- Do the pop
local stack = _getStack(self)
table.remove(stack, #stack)
-- Invoke continuedState on the new state
local newState = self:getCurrentState()
_invokeCallback(self, newState, 'continuedState')
return newState
end
--[[ Empties the state stack
This function will invoke all the popState, exitState callbacks on all the states as they pop out.
]]
function StatefulObject:popAllStates()
local state = self:popState()
while(state~=nil) do state = self:popState() end
end
--[[ Returns the current state (top of the stack only)
The current state's name can be obtained doing object:getCurrentState().name
]]
function StatefulObject:getCurrentState()
local stack = _getStack(self)
if #stack == 0 then return nil end
return(stack[#stack])
end
-- Invoke pausedState on the previous state
_invokeCallback(self, self:getCurrentState(), 'pausedState')
-- Do the push
table.insert(stack, nextState)
-- Invoke pushedState & enterState on the next state
_invokeCallback(self, nextState, 'pushedState')
_invokeCallback(self, nextState, 'enterState')
return nextState
end
--[[
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:isInState(stateName, testStateStack)
local stack = _getStack(self)
if(testStateStack==true) then
--[[ Removes a state from the state stack
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(_private[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 exitstate & poppedState on the state being popped out
local prevState = self:getCurrentState()
_invokeCallback(self, prevState, 'exitState')
_invokeCallback(self, prevState, 'poppedState')
-- Do the pop
local stack = _getStack(self)
table.remove(stack, #stack)
-- Invoke continuedState on the new state
local newState = self:getCurrentState()
_invokeCallback(self, newState, 'continuedState')
return newState
end
--[[ Empties the state stack
This function will invoke all the popState, exitState callbacks on all the states as they pop out.
]]
function StatefulObject:popAllStates()
local state = self:popState()
while(state~=nil) do state = self:popState() end
end
--[[ Returns the current state (top of the stack only)
The current state's name can be obtained doing object:getCurrentState().name
]]
function StatefulObject:getCurrentState()
local stack = _getStack(self)
if #stack == 0 then return nil end
return(stack[#stack])
end
--[[
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:isInState(stateName, testStateStack)
local stack = _getStack(self)
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
end
else --testStateStack==false
local state = stack[#stack]
if(state~=nil and state.name == stateName) then return true end
end
return false
end
------------------------------------
-- 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
superState is optional. If nil, State will be the parent class of the new state
returns the newly created state
]]
function StatefulObject.addState(theClass, stateName, superState)
superState = superState or State
--print(theClass.name, stateName, superState.name)
assert(subclassOf(StatefulObject, theClass), "Use class:addState instead of class.addState")
assert(theClass.states[stateName]==nil, "The class " .. theClass.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)
local state = superState:subclass(stateName, theClass)
theClass.states[stateName] = state
return state
end
end
--[[ Redefinition of Object: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(theClass, name)
function StatefulObject.subclass(theClass, name)
assert(theClass==StatefulObject or subclassOf(StatefulObject, theClass), "Use class:subclass instead of class.subclass")
local theSubClass = _prevSubclass(theClass, name) --for now, theClass is just a regular subclass
--the states of the subclass are subclasses of the superclass' states
@ -234,15 +258,15 @@ function StatefulObject.subclass(theClass, name)
--look for instance methods on the state stack before looking them up on the class' dictionary
local classDict = theSubClass.__classDict
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
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
end
--if ignored or not found, look on the class method
return classDict[methodName]
@ -272,4 +296,4 @@ function StatefulObject.includes(theClass, module, ...)
state:includes(moduleState, ...)
end
end
end
end