2010-07-18 17:19:49 +02:00
|
|
|
// Hierarchical finite state machine implementation.
|
|
|
|
//
|
|
|
|
// FSMs are specified as a JS data structure;
|
|
|
|
// see e.g. AnimalAI.js for an example of the syntax.
|
|
|
|
//
|
|
|
|
// FSMs are implicitly linked with an external object.
|
|
|
|
// That object stores all FSM-related state.
|
|
|
|
// (This means we can serialise FSM-based components as
|
|
|
|
// plain old JS objects, with no need to serialise the complex
|
|
|
|
// FSM structure itself or to add custom serialisation code.)
|
|
|
|
|
|
|
|
function FSM(spec)
|
|
|
|
{
|
|
|
|
// The (relatively) human-readable FSM specification needs to get
|
|
|
|
// compiled into a more-efficient-to-execute version.
|
|
|
|
//
|
|
|
|
// In particular, message handling should require minimal
|
|
|
|
// property lookups in the common case (even when the FSM has
|
|
|
|
// a deeply nested hierarchy), and there should never be any
|
|
|
|
// string manipulation at run-time.
|
|
|
|
|
|
|
|
this.decompose = { "": [] };
|
|
|
|
/* 'decompose' will store:
|
|
|
|
{
|
|
|
|
"": [],
|
|
|
|
"A": ["A"],
|
|
|
|
"A.B": ["A", "A.B"],
|
|
|
|
"A.B.C": ["A", "A.B", "A.B.C"],
|
|
|
|
"A.B.D": ["A", "A.B", "A.B.D"],
|
|
|
|
...
|
|
|
|
};
|
|
|
|
This is used when switching between states in different branches
|
|
|
|
of the hierarchy, to determine the list of sub-states to leave/enter
|
|
|
|
*/
|
|
|
|
|
|
|
|
this.states = { };
|
|
|
|
/* 'states' will store:
|
|
|
|
{
|
|
|
|
...
|
|
|
|
"A": {
|
|
|
|
"_name": "A",
|
|
|
|
"_parent": "",
|
|
|
|
"_refs": { // local -> global name lookups (for SetNextState)
|
|
|
|
"B": "A.B",
|
|
|
|
"B.C": "A.B.C",
|
|
|
|
"B.D": "A.B.D",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"A.B": {
|
|
|
|
"_name": "A.B",
|
|
|
|
"_parent": "A",
|
|
|
|
"_refs": {
|
|
|
|
"C": "A.B.C",
|
|
|
|
"D": "A.B.D",
|
|
|
|
},
|
|
|
|
"MessageType": function(msg) { ... },
|
|
|
|
},
|
|
|
|
"A.B.C": {
|
|
|
|
"_name": "A.B.C",
|
|
|
|
"_parent": "A.B",
|
|
|
|
"_refs": {},
|
|
|
|
"enter": function() { ... },
|
|
|
|
"MessageType": function(msg) { ... },
|
|
|
|
},
|
|
|
|
"A.B.D": {
|
|
|
|
"_name": "A.B.D",
|
|
|
|
"_parent": "A.B",
|
|
|
|
"_refs": {},
|
|
|
|
"enter": function() { ... },
|
|
|
|
"leave": function() { ... },
|
|
|
|
"MessageType": function(msg) { ... },
|
|
|
|
},
|
|
|
|
...
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function process(fsm, node, path, handlers)
|
|
|
|
{
|
|
|
|
var state = {};
|
|
|
|
fsm.states[path.join(".")] = state;
|
|
|
|
|
|
|
|
var newhandlers = {};
|
|
|
|
for (var e in handlers)
|
|
|
|
newhandlers[e] = handlers[e];
|
|
|
|
|
|
|
|
state._name = path.join(".");
|
|
|
|
state._parent = path.slice(0, -1).join(".");
|
|
|
|
state._refs = {};
|
|
|
|
|
|
|
|
for (var key in node)
|
|
|
|
{
|
|
|
|
if (key === "enter" || key === "leave")
|
|
|
|
{
|
|
|
|
state[key] = node[key];
|
|
|
|
}
|
|
|
|
else if (key.match(/^[A-Z]+$/))
|
|
|
|
{
|
|
|
|
state._refs[key] = (state._name ? state._name + "." : "") + key;
|
|
|
|
|
|
|
|
// (the rest of this will be handled later once we've grabbed
|
|
|
|
// all the event handlers)
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
newhandlers[key] = node[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var e in newhandlers)
|
|
|
|
state[e] = newhandlers[e];
|
|
|
|
|
|
|
|
for (var key in node)
|
|
|
|
{
|
|
|
|
if (key.match(/^[A-Z]+$/))
|
|
|
|
{
|
|
|
|
var newpath = path.concat([key]);
|
|
|
|
|
|
|
|
var decomposed = [newpath[0]];
|
|
|
|
for (var i = 1; i < newpath.length; ++i)
|
|
|
|
decomposed.push(decomposed[i-1] + "." + newpath[i]);
|
|
|
|
fsm.decompose[newpath.join(".")] = decomposed;
|
|
|
|
|
|
|
|
var childstate = process(fsm, node[key], newpath, newhandlers);
|
|
|
|
|
|
|
|
for (var r in childstate._refs)
|
|
|
|
{
|
|
|
|
var cname = key + "." + r;
|
|
|
|
state._refs[cname] = childstate._refs[r];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
process(this, spec, [], {});
|
|
|
|
}
|
|
|
|
|
|
|
|
FSM.prototype.Init = function(obj, initialState)
|
|
|
|
{
|
|
|
|
this.deferFromState = undefined;
|
|
|
|
|
|
|
|
obj.fsmStateName = "";
|
|
|
|
obj.fsmNextState = undefined;
|
|
|
|
this.SwitchToNextState(obj, initialState);
|
|
|
|
};
|
|
|
|
|
|
|
|
FSM.prototype.SetNextState = function(obj, state)
|
|
|
|
{
|
|
|
|
obj.fsmNextState = state;
|
|
|
|
};
|
|
|
|
|
|
|
|
FSM.prototype.ProcessMessage = function(obj, msg)
|
|
|
|
{
|
2010-07-21 18:09:58 +02:00
|
|
|
// warn("ProcessMessage(obj, "+uneval(msg)+")");
|
2010-07-18 17:19:49 +02:00
|
|
|
|
|
|
|
var func = this.states[obj.fsmStateName][msg.type];
|
|
|
|
if (!func)
|
|
|
|
{
|
|
|
|
error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
func.apply(obj, [msg]);
|
|
|
|
|
|
|
|
while (obj.fsmNextState)
|
|
|
|
{
|
|
|
|
var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState);
|
|
|
|
obj.fsmNextState = undefined;
|
|
|
|
|
|
|
|
if (nextStateName != obj.fsmStateName)
|
|
|
|
this.SwitchToNextState(obj, nextStateName);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
FSM.prototype.DeferMessage = function(obj, msg)
|
|
|
|
{
|
|
|
|
// We need to work out which sub-state we were running the message handler from,
|
|
|
|
// and then try again in its parent state.
|
|
|
|
var old = this.deferFromState;
|
|
|
|
var from;
|
|
|
|
if (old) // if we're recursively deferring and saved the last used state, use that
|
|
|
|
from = old;
|
|
|
|
else // if this is the first defer then we must have last processed the message in the current FSM state
|
|
|
|
from = obj.fsmStateName;
|
|
|
|
|
|
|
|
// Find and save the parent, for use in recursive defers
|
|
|
|
this.deferFromState = this.states[from]._parent;
|
|
|
|
|
|
|
|
// Run the function from the parent state
|
|
|
|
var state = this.states[this.deferFromState];
|
|
|
|
var func = state[msg.type];
|
|
|
|
if (!func)
|
|
|
|
error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'");
|
|
|
|
func.apply(obj, [msg]);
|
|
|
|
|
|
|
|
// Restore the changes we made
|
|
|
|
this.deferFromState = old;
|
|
|
|
|
|
|
|
// TODO: if an inherited handler defers, it calls exactly the same handler
|
|
|
|
// on the parent state, which is probably useless and inefficient
|
|
|
|
|
|
|
|
// NOTE: this will break if two units try to execute AI at the same time;
|
|
|
|
// as long as AI messages are queue and processed asynchronously it should be fine
|
|
|
|
};
|
|
|
|
|
|
|
|
FSM.prototype.LookupState = function(currentStateName, stateName)
|
|
|
|
{
|
|
|
|
// print("LookupState("+currentStateName+", "+stateName+")\n");
|
|
|
|
for (var s = currentStateName; s; s = this.states[s]._parent)
|
|
|
|
if (stateName in this.states[s]._refs)
|
|
|
|
return this.states[s]._refs[stateName];
|
|
|
|
return stateName;
|
|
|
|
};
|
|
|
|
|
|
|
|
FSM.prototype.SwitchToNextState = function(obj, nextStateName)
|
|
|
|
{
|
|
|
|
var fromState = this.decompose[obj.fsmStateName];
|
|
|
|
var toState = this.decompose[nextStateName];
|
|
|
|
|
|
|
|
if (!toState)
|
|
|
|
error("Tried to change to non-existent state '" + nextState + "'");
|
|
|
|
|
2010-07-29 22:39:23 +02:00
|
|
|
// Find the set of states in the hierarchy tree to leave then enter,
|
|
|
|
// to traverse from the old state to the new one.
|
|
|
|
// If any enter/leave function returns true then abort the process
|
|
|
|
// (this lets them intercept the transition and start a new transition)
|
|
|
|
|
2010-07-18 17:19:49 +02:00
|
|
|
for (var equalPrefix = 0; fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = fromState.length-1; i >= equalPrefix; --i)
|
|
|
|
{
|
|
|
|
var leave = this.states[fromState[i]].leave;
|
|
|
|
if (leave)
|
2010-07-29 22:39:23 +02:00
|
|
|
{
|
|
|
|
obj.fsmStateName = fromState[i];
|
|
|
|
if (leave.apply(obj))
|
|
|
|
return;
|
|
|
|
}
|
2010-07-18 17:19:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = equalPrefix; i < toState.length; ++i)
|
|
|
|
{
|
|
|
|
var enter = this.states[toState[i]].enter;
|
|
|
|
if (enter)
|
2010-07-29 22:39:23 +02:00
|
|
|
{
|
|
|
|
obj.fsmStateName = toState[i];
|
|
|
|
if (enter.apply(obj))
|
|
|
|
return;
|
|
|
|
}
|
2010-07-18 17:19:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
obj.fsmStateName = nextStateName;
|
|
|
|
}
|
|
|
|
|
|
|
|
Engine.RegisterGlobal("FSM", FSM);
|