Comparison

util/fsm.lua @ 13019:8a2f75e38eb2

util.fsm: New utility lib for finite state machines
author Matthew Wild <mwild1@gmail.com>
date Thu, 17 Mar 2022 17:45:27 +0000
child 13165:9c13c11b199d
comparison
equal deleted inserted replaced
13018:9ed4a8502c54 13019:8a2f75e38eb2
1 local events = require "util.events";
2
3 local fsm_methods = {};
4 local fsm_mt = { __index = fsm_methods };
5
6 local function is_fsm(o)
7 local mt = getmetatable(o);
8 return mt == fsm_mt;
9 end
10
11 local function notify_transition(fire_event, transition_event)
12 local ret;
13 ret = fire_event("transition", transition_event);
14 if ret ~= nil then return ret; end
15 if transition_event.from ~= transition_event.to then
16 ret = fire_event("leave/"..transition_event.from, transition_event);
17 if ret ~= nil then return ret; end
18 end
19 ret = fire_event("transition/"..transition_event.name, transition_event);
20 if ret ~= nil then return ret; end
21 end
22
23 local function notify_transitioned(fire_event, transition_event)
24 if transition_event.to ~= transition_event.from then
25 fire_event("enter/"..transition_event.to, transition_event);
26 end
27 if transition_event.name then
28 fire_event("transitioned/"..transition_event.name, transition_event);
29 end
30 fire_event("transitioned", transition_event);
31 end
32
33 local function do_transition(name)
34 return function (self, attr)
35 local new_state = self.fsm.states[self.state][name] or self.fsm.states["*"][name];
36 if not new_state then
37 return error(("Invalid state transition: %s cannot %s"):format(self.state, name));
38 end
39
40 local transition_event = {
41 instance = self;
42
43 name = name;
44 to = new_state;
45 to_attr = attr;
46
47 from = self.state;
48 from_attr = self.state_attr;
49 };
50
51 local fire_event = self.fsm.events.fire_event;
52 local ret = notify_transition(fire_event, transition_event);
53 if ret ~= nil then return nil, ret; end
54
55 self.state = new_state;
56 self.state_attr = attr;
57
58 notify_transitioned(fire_event, transition_event);
59 return true;
60 end;
61 end
62
63 local function new(desc)
64 local self = setmetatable({
65 default_state = desc.default_state;
66 events = events.new();
67 }, fsm_mt);
68
69 -- states[state_name][transition_name] = new_state_name
70 local states = { ["*"] = {} };
71 if desc.default_state then
72 states[desc.default_state] = {};
73 end
74 self.states = states;
75
76 local instance_methods = {};
77 self._instance_mt = { __index = instance_methods };
78
79 for _, transition in ipairs(desc.transitions or {}) do
80 local from_states = transition.from;
81 if type(from_states) ~= "table" then
82 from_states = { from_states };
83 end
84 for _, from in ipairs(from_states) do
85 if not states[from] then
86 states[from] = {};
87 end
88 if not states[transition.to] then
89 states[transition.to] = {};
90 end
91 if states[from][transition.name] then
92 return error(("Duplicate transition in FSM specification: %s from %s"):format(transition.name, from));
93 end
94 states[from][transition.name] = transition.to;
95 end
96
97 -- Add public method to trigger this transition
98 instance_methods[transition.name] = do_transition(transition.name);
99 end
100
101 if desc.state_handlers then
102 for state_name, handler in pairs(desc.state_handlers) do
103 self.events.add_handler("enter/"..state_name, handler);
104 end
105 end
106
107 if desc.transition_handlers then
108 for transition_name, handler in pairs(desc.transition_handlers) do
109 self.events.add_handler("transition/"..transition_name, handler);
110 end
111 end
112
113 if desc.handlers then
114 self.events.add_handlers(desc.handlers);
115 end
116
117 return self;
118 end
119
120 function fsm_methods:init(state_name, state_attr)
121 local initial_state = assert(state_name or self.default_state, "no initial state specified");
122 if not self.states[initial_state] then
123 return error("Invalid initial state: "..initial_state);
124 end
125 local instance = setmetatable({
126 fsm = self;
127 state = initial_state;
128 state_attr = state_attr;
129 }, self._instance_mt);
130
131 if initial_state ~= self.default_state then
132 local fire_event = self.events.fire_event;
133 notify_transitioned(fire_event, {
134 instance = instance;
135
136 to = initial_state;
137 to_attr = state_attr;
138
139 from = self.default_state;
140 });
141 end
142
143 return instance;
144 end
145
146 function fsm_methods:is_instance(o)
147 local mt = getmetatable(o);
148 return mt == self._instance_mt;
149 end
150
151 return {
152 new = new;
153 is_fsm = is_fsm;
154 };