0ad/binaries/data/mods/public/simulation/helpers/FSM.js

258 lines
6.4 KiB
JavaScript
Raw Normal View History

// 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)
{
// warn("ProcessMessage(obj, "+uneval(msg)+")");
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 + "'");
// 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)
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)
{
obj.fsmStateName = fromState[i];
if (leave.apply(obj))
return;
}
}
for (var i = equalPrefix; i < toState.length; ++i)
{
var enter = this.states[toState[i]].enter;
if (enter)
{
obj.fsmStateName = toState[i];
if (enter.apply(obj))
return;
}
}
obj.fsmStateName = nextStateName;
}
Engine.RegisterGlobal("FSM", FSM);