LCOV - code coverage report
Current view: top level - globalscripts - FSM.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 83 101 82.2 %
Date: 2023-04-02 12:52:40 Functions: 8 9 88.9 %

          Line data    Source code
       1             : // Hierarchical finite state machine implementation.
       2             : //
       3             : // FSMs are specified as a JS data structure;
       4             : // see e.g. UnitAI.js for an example of the syntax.
       5             : //
       6             : // FSMs are implicitly linked with an external object.
       7             : // That object stores all FSM-related state.
       8             : // (This means we can serialise FSM-based components as
       9             : // plain old JS objects, with no need to serialise the complex
      10             : // FSM structure itself or to add custom serialisation code.)
      11             : 
      12             : /**
      13             : 
      14             : FSM API:
      15             : 
      16             : Users define the FSM behaviour like:
      17             : 
      18             : var FsmSpec = {
      19             : 
      20             :         // Define some default message handlers:
      21             : 
      22             :         "MessageName1": function(msg) {
      23             :                 // This function will be called in response to calls to
      24             :                 //   Fsm.ProcessMessage(this, { "type": "MessageName1", "data": msg });
      25             :                 //
      26             :                 // In this function, 'this' is the component object passed into
      27             :                 // ProcessMessage, so you can access 'this.propertyName'
      28             :                 // and 'this.methodName()' etc.
      29             :         },
      30             : 
      31             :         "MessageName2": function(msg) {
      32             :                 // Another message handler.
      33             :         },
      34             : 
      35             :         // Define the behaviour for the 'STATENAME' state:
      36             :         // Names of states may only contain the characters A-Z
      37             :         "STATENAME": {
      38             : 
      39             :                 "MessageName1": function(msg) {
      40             :                         // This overrides the previous MessageName1 that was
      41             :                         // defined earlier, and will be called instead of it
      42             :                         // in response to ProcessMessage.
      43             :                 },
      44             : 
      45             :                 // We don't override MessageName2, so the default one
      46             :                 // will be called instead.
      47             : 
      48             :                 // Define the 'STATENAME.SUBSTATENAME' state:
      49             :                 // (we support arbitrarily-nested hierarchies of states)
      50             :                 "SUBSTATENAME": {
      51             : 
      52             :                         "MessageName2": function(msg) {
      53             :                                 // Override the default MessageName2.
      54             :                                 // But we don't override MessageName1, so the one from
      55             :                                 // STATENAME will be used instead.
      56             :                         },
      57             : 
      58             :                         "enter": function() {
      59             :                                 // This is a special function called when transitioning
      60             :                                 // into this state, or into a substate of this state.
      61             :                                 //
      62             :                                 // If it returns true, the transition will be aborted:
      63             :                                 // do this if you've called SetNextState inside this enter
      64             :                                 // handler, because otherwise the new state transition
      65             :                                 // will get mixed up with the previous ongoing one.
      66             :                                 // In normal cases, you can return false or nothing.
      67             :                         },
      68             : 
      69             :                         "leave": function() {
      70             :                                 // Called when transitioning out of this state.
      71             :                         },
      72             :                 },
      73             : 
      74             :                 // Define a new state which is an exact copy of another
      75             :                 // state that is defined elsewhere in this FSM:
      76             :                 "OTHERSUBSTATENAME": "STATENAME.SUBSTATENAME",
      77             :         }
      78             : 
      79             : }
      80             : 
      81             : 
      82             : Objects can then make themselves act as an instance of the FSM by running
      83             :         FsmSpec.Init(this, "STATENAME");
      84             : which will define a few properties on 'this' (with names prefixed "fsm"),
      85             : and then they can call the FSM functions on the object like
      86             :         FsmSpec.SetNextState(this, "STATENAME.SUBSTATENAME");
      87             : 
      88             : These objects must also define a function property that can be called as
      89             :         this.FsmStateNameChanged(name);
      90             : 
      91             : (This design aims to avoid storing any per-instance state that cannot be
      92             : easily serialized - it only stores state-name strings.)
      93             : 
      94             :  */
      95             : 
      96             : function FSM(spec)
      97             : {
      98             :         // The (relatively) human-readable FSM specification needs to get
      99             :         // compiled into a more-efficient-to-execute version.
     100             :         //
     101             :         // In particular, message handling should require minimal
     102             :         // property lookups in the common case (even when the FSM has
     103             :         // a deeply nested hierarchy), and there should never be any
     104             :         // string manipulation at run-time.
     105             : 
     106           1 :         this.decompose = { "": [] };
     107             :         /* 'decompose' will store:
     108             :                 {
     109             :                         "": [],
     110             :                         "A": ["A"],
     111             :                         "A.B": ["A", "A.B"],
     112             :                         "A.B.C": ["A", "A.B", "A.B.C"],
     113             :                         "A.B.D": ["A", "A.B", "A.B.D"],
     114             :                         ...
     115             :                 };
     116             :                 This is used when switching between states in different branches
     117             :                 of the hierarchy, to determine the list of sub-states to leave/enter
     118             :         */
     119             : 
     120           1 :         this.states = { };
     121             :         /* 'states' will store:
     122             :                 {
     123             :                         ...
     124             :                         "A": {
     125             :                                 "_name": "A",
     126             :                                 "_parent": "",
     127             :                                 "_refs": { // local -> global name lookups (for SetNextState)
     128             :                                         "B": "A.B",
     129             :                                         "B.C": "A.B.C",
     130             :                                         "B.D": "A.B.D",
     131             :                                 },
     132             :                         },
     133             :                         "A.B": {
     134             :                                 "_name": "A.B",
     135             :                                 "_parent": "A",
     136             :                                 "_refs": {
     137             :                                         "C": "A.B.C",
     138             :                                         "D": "A.B.D",
     139             :                                 },
     140             :                                 "MessageType": function(msg) { ... },
     141             :                         },
     142             :                         "A.B.C": {
     143             :                                 "_name": "A.B.C",
     144             :                                 "_parent": "A.B",
     145             :                                 "_refs": {},
     146             :                                 "enter": function() { ... },
     147             :                                 "MessageType": function(msg) { ... },
     148             :                         },
     149             :                         "A.B.D": {
     150             :                                 "_name": "A.B.D",
     151             :                                 "_parent": "A.B",
     152             :                                 "_refs": {},
     153             :                                 "enter": function() { ... },
     154             :                                 "leave": function() { ... },
     155             :                                 "MessageType": function(msg) { ... },
     156             :                         },
     157             :                         ...
     158             :                 }
     159             :         */
     160             : 
     161             :         function process(fsm, node, path, handlers)
     162             :         {
     163             :                 // Handle string references to nodes defined elsewhere in the FSM spec
     164          77 :                 if (typeof node === "string")
     165             :                 {
     166           3 :                         var refpath = node.split(".");
     167           3 :                         var refd = spec;
     168           3 :                         for (var p of refpath)
     169             :                         {
     170           7 :                                 refd = refd[p];
     171           7 :                                 if (!refd)
     172             :                                 {
     173           0 :                                         error("FSM node "+path.join(".")+" referred to non-defined node "+node);
     174           0 :                                         return {};
     175             :                                 }
     176             :                         }
     177           3 :                         node = refd;
     178             :                 }
     179             : 
     180          77 :                 var state = {};
     181          77 :                 fsm.states[path.join(".")] = state;
     182             : 
     183          77 :                 var newhandlers = {};
     184          77 :                 for (var e in handlers)
     185        3013 :                         newhandlers[e] = handlers[e];
     186             : 
     187          77 :                 state._name = path.join(".");
     188          77 :                 state._parent = path.slice(0, -1).join(".");
     189          77 :                 state._refs = {};
     190             : 
     191          77 :                 for (var key in node)
     192             :                 {
     193         363 :                         if (key === "enter" || key === "leave")
     194             :                         {
     195         123 :                                 state[key] = node[key];
     196             :                         }
     197         240 :                         else if (key.match(/^[A-Z]+$/))
     198             :                         {
     199          76 :                                 state._refs[key] = (state._name ? state._name + "." : "") + key;
     200             : 
     201             :                                 // (the rest of this will be handled later once we've grabbed
     202             :                                 // all the event handlers)
     203             :                         }
     204             :                         else
     205             :                         {
     206         164 :                                 newhandlers[key] = node[key];
     207             :                         }
     208             :                 }
     209             : 
     210          77 :                 for (var e in newhandlers)
     211        3088 :                         state[e] = newhandlers[e];
     212             : 
     213          77 :                 for (var key in node)
     214             :                 {
     215         363 :                         if (key.match(/^[A-Z]+$/))
     216             :                         {
     217          76 :                                 var newpath = path.concat([key]);
     218             : 
     219          76 :                                 var decomposed = [newpath[0]];
     220          76 :                                 for (var i = 1; i < newpath.length; ++i)
     221         119 :                                         decomposed.push(decomposed[i-1] + "." + newpath[i]);
     222          76 :                                 fsm.decompose[newpath.join(".")] = decomposed;
     223             : 
     224          76 :                                 var childstate = process(fsm, node[key], newpath, newhandlers);
     225             : 
     226          76 :                                 for (var r in childstate._refs)
     227             :                                 {
     228         119 :                                         var cname = key + "." + r;
     229         119 :                                         state._refs[cname] = childstate._refs[r];
     230             :                                 }
     231             :                         }
     232             :                 }
     233             : 
     234          77 :                 return state;
     235             :         }
     236             : 
     237           1 :         process(this, spec, [], {});
     238             : }
     239             : 
     240          72 : FSM.prototype.Init = function(obj, initialState)
     241             : {
     242          19 :         this.deferFromState = undefined;
     243             : 
     244          19 :         obj.fsmStateName = "";
     245          19 :         obj.fsmNextState = undefined;
     246          19 :         this.SwitchToNextState(obj, initialState);
     247             : };
     248             : 
     249          72 : FSM.prototype.SetNextState = function(obj, state)
     250             : {
     251          46 :         obj.fsmNextState = state;
     252             : };
     253             : 
     254          72 : FSM.prototype.ProcessMessage = function(obj, msg)
     255             : {
     256             : //      warn("ProcessMessage(obj, "+uneval(msg)+")");
     257             : 
     258          54 :         var func = this.states[obj.fsmStateName][msg.type];
     259          54 :         if (!func)
     260             :         {
     261           0 :                 error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'");
     262           0 :                 return undefined;
     263             :         }
     264             : 
     265          54 :         var ret = func.apply(obj, [msg]);
     266             : 
     267             :         // If func called SetNextState then switch into the new state,
     268             :         // and continue switching if the new state's 'enter' called SetNextState again
     269          54 :         while (obj.fsmNextState)
     270             :         {
     271          43 :                 var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState);
     272          43 :                 obj.fsmNextState = undefined;
     273             : 
     274          43 :                 this.SwitchToNextState(obj, nextStateName);
     275             :         }
     276             : 
     277          54 :         return ret;
     278             : };
     279             : 
     280          72 : FSM.prototype.DeferMessage = function(obj, msg)
     281             : {
     282             :         // We need to work out which sub-state we were running the message handler from,
     283             :         // and then try again in its parent state.
     284           0 :         var old = this.deferFromState;
     285             :         var from;
     286           0 :         if (old) // if we're recursively deferring and saved the last used state, use that
     287           0 :                 from = old;
     288             :         else // if this is the first defer then we must have last processed the message in the current FSM state
     289           0 :                 from = obj.fsmStateName;
     290             : 
     291             :         // Find and save the parent, for use in recursive defers
     292           0 :         this.deferFromState = this.states[from]._parent;
     293             : 
     294             :         // Run the function from the parent state
     295           0 :         var state = this.states[this.deferFromState];
     296           0 :         var func = state[msg.type];
     297           0 :         if (!func)
     298           0 :                 error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'");
     299           0 :         func.apply(obj, [msg]);
     300             : 
     301             :         // Restore the changes we made
     302           0 :         this.deferFromState = old;
     303             : 
     304             :         // TODO: if an inherited handler defers, it calls exactly the same handler
     305             :         // on the parent state, which is probably useless and inefficient
     306             : 
     307             :         // NOTE: this will break if two units try to execute AI at the same time;
     308             :         // as long as AI messages are queue and processed asynchronously it should be fine
     309             : };
     310             : 
     311          72 : FSM.prototype.LookupState = function(currentStateName, stateName)
     312             : {
     313             : //      print("LookupState("+currentStateName+", "+stateName+")\n");
     314          43 :         for (var s = currentStateName; s; s = this.states[s]._parent)
     315          97 :                 if (stateName in this.states[s]._refs)
     316           7 :                         return this.states[s]._refs[stateName];
     317          36 :         return stateName;
     318             : };
     319             : 
     320          72 : FSM.prototype.GetCurrentState = function(obj)
     321             : {
     322           8 :         return obj.fsmStateName;
     323             : };
     324             : 
     325          72 : FSM.prototype.SwitchToNextState = function(obj, nextStateName)
     326             : {
     327          66 :         var fromState = this.decompose[obj.fsmStateName];
     328          66 :         var toState = this.decompose[nextStateName];
     329             : 
     330          66 :         if (!toState)
     331           0 :                 error("Tried to change to non-existent state '" + nextStateName + "'");
     332             : 
     333             :         // Find the set of states in the hierarchy tree to leave then enter,
     334             :         // to traverse from the old state to the new one.
     335             :         // If any enter/leave function returns true then abort the process
     336             :         // (this lets them intercept the transition and start a new transition)
     337             : 
     338          66 :         for (var equalPrefix = 0; fromState[equalPrefix] && fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix)
     339             :         {
     340             :         }
     341             : 
     342             :         // If the next-state is the same as the current state, leave/enter up one level so cleanup gets triggered.
     343          66 :         if (equalPrefix > 0 && equalPrefix === toState.length)
     344           2 :                 --equalPrefix;
     345             : 
     346          66 :         for (var i = fromState.length-1; i >= equalPrefix; --i)
     347             :         {
     348          82 :                 var leave = this.states[fromState[i]].leave;
     349          82 :                 if (leave)
     350             :                 {
     351          66 :                         obj.fsmStateName = fromState[i];
     352          66 :                         if (leave.apply(obj))
     353             :                         {
     354           0 :                                 obj.FsmStateNameChanged(obj.fsmStateName);
     355           0 :                                 return;
     356             :                         }
     357             :                 }
     358             :         }
     359             : 
     360          66 :         for (var i = equalPrefix; i < toState.length; ++i)
     361             :         {
     362         122 :                 var enter = this.states[toState[i]].enter;
     363         122 :                 if (enter)
     364             :                 {
     365          73 :                         obj.fsmStateName = toState[i];
     366          73 :                         if (enter.apply(obj))
     367             :                         {
     368           2 :                                 obj.FsmStateNameChanged(obj.fsmStateName);
     369           2 :                                 return;
     370             :                         }
     371             :                 }
     372             :         }
     373             : 
     374          64 :         obj.fsmStateName = nextStateName;
     375          64 :         obj.FsmStateNameChanged(obj.fsmStateName);
     376             : };

Generated by: LCOV version 1.14