You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
5.4 KiB
Lua
221 lines
5.4 KiB
Lua
-- https://github.com/unindented/lua-fsm
|
|
local unpack = unpack or table.unpack
|
|
local type = type
|
|
local assert = assert
|
|
local ipairs = ipairs
|
|
local pairs = pairs
|
|
local tinsert = table.insert
|
|
-------------------------------------------
|
|
|
|
local M = {}
|
|
|
|
M.WILDCARD = "*"
|
|
M.DEFERRED = 0
|
|
M.SUCCEEDED = 1
|
|
M.NO_TRANSITION = 2
|
|
M.PENDING = 3
|
|
M.CANCELLED = 4
|
|
|
|
local function do_callback(handler, args)
|
|
if handler then
|
|
return handler(unpack(args))
|
|
end
|
|
end
|
|
|
|
local function before_event(self, event, _, _, args)
|
|
local specific = do_callback(self["on_before_" .. event], args)
|
|
local general = do_callback(self["on_before_event"], args)
|
|
|
|
if specific == false or general == false then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local function leave_state(self, _, from, _, args)
|
|
local specific = do_callback(self["on_leave_" .. from], args)
|
|
local general = do_callback(self["on_leave_state"], args)
|
|
|
|
if specific == false or general == false then
|
|
return false
|
|
end
|
|
if specific == M.DEFERRED or general == M.DEFERRED then
|
|
return M.DEFERRED
|
|
end
|
|
end
|
|
|
|
local function enter_state(self, _, _, to, args)
|
|
do_callback(self["on_enter_" .. to] or self["on_" .. to], args)
|
|
do_callback(self["on_enter_state"] or self["on_state"], args)
|
|
end
|
|
|
|
local function after_event(self, event, _, _, args)
|
|
do_callback(self["on_after_" .. event] or self["on_" .. event], args)
|
|
do_callback(self["on_after_event"] or self["on_event"], args)
|
|
end
|
|
|
|
local function build_transition(self, event, states)
|
|
return function(...)
|
|
local from = self.current
|
|
local to = states[from] or states[M.WILDCARD] or from
|
|
local args = {self, event, from, to, ...}
|
|
|
|
assert(not self.is_pending(), "previous transition still pending")
|
|
|
|
assert(self.can(event), "invalid transition from state '" .. from .. "' with event '" .. event .. "'")
|
|
|
|
local before = before_event(self, event, from, to, args)
|
|
if before == false then
|
|
return M.CANCELLED
|
|
end
|
|
|
|
if from == to then
|
|
after_event(self, event, from, to, args)
|
|
return M.NO_TRANSITION
|
|
end
|
|
|
|
self.confirm = function()
|
|
self.confirm = nil
|
|
self.cancel = nil
|
|
|
|
self.current = to
|
|
|
|
enter_state(self, event, from, to, args)
|
|
after_event(self, event, from, to, args)
|
|
|
|
return M.SUCCEEDED
|
|
end
|
|
|
|
self.cancel = function()
|
|
self.confirm = nil
|
|
self.cancel = nil
|
|
|
|
after_event(self, event, from, to, args)
|
|
|
|
return M.CANCELLED
|
|
end
|
|
|
|
local leave = leave_state(self, event, from, to, args)
|
|
if leave == false then
|
|
return M.CANCELLED
|
|
end
|
|
if leave == M.DEFERRED then
|
|
return M.PENDING
|
|
end
|
|
|
|
if self.confirm then
|
|
return self.confirm()
|
|
end
|
|
end
|
|
end
|
|
|
|
function M.create(cfg, target)
|
|
local self = target or {}
|
|
|
|
-- Initial state.
|
|
local initial = cfg.initial
|
|
-- Allow for a string, or a map like `{state = "foo", event = "setup"}`.
|
|
initial = type(initial) == "string" and {
|
|
state = initial,
|
|
} or initial
|
|
|
|
-- Initial event.
|
|
local initial_event = initial and initial.event or "startup"
|
|
|
|
-- Terminal state.
|
|
local terminal = cfg.terminal
|
|
|
|
-- Events.
|
|
local events = cfg.events or {}
|
|
|
|
-- Callbacks.
|
|
local callbacks = cfg.callbacks or {}
|
|
|
|
-- Track state transitions allowed for an event.
|
|
local states_for_event = {}
|
|
|
|
-- Track events allowed from a state.
|
|
local events_for_state = {}
|
|
|
|
local function add(e)
|
|
-- Allow wildcard transition if `from` is not specified.
|
|
local from = type(e.from) == "table" and e.from or (e.from and {e.from} or {M.WILDCARD})
|
|
local to = e.to
|
|
local event = e.name
|
|
|
|
states_for_event[event] = states_for_event[event] or {}
|
|
for _, fr in ipairs(from) do
|
|
events_for_state[fr] = events_for_state[fr] or {}
|
|
tinsert(events_for_state[fr], event)
|
|
|
|
-- Allow no-op transition if `to` is not specified.
|
|
states_for_event[event][fr] = to or fr
|
|
end
|
|
end
|
|
|
|
if initial then
|
|
add({
|
|
name = initial_event,
|
|
from = "none",
|
|
to = initial.state,
|
|
})
|
|
end
|
|
|
|
for _, event in ipairs(events) do
|
|
add(event)
|
|
end
|
|
|
|
for event, states in pairs(states_for_event) do
|
|
self[event] = build_transition(self, event, states)
|
|
end
|
|
|
|
for name, callback in pairs(callbacks) do
|
|
self[name] = callback
|
|
end
|
|
|
|
self.current = "none"
|
|
|
|
function self.is(state)
|
|
if type(state) == "table" then
|
|
for _, s in ipairs(state) do
|
|
if self.current == s then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
return self.current == state
|
|
end
|
|
|
|
function self.can(event)
|
|
local states = states_for_event[event]
|
|
local to = states[self.current] or states[M.WILDCARD]
|
|
return to ~= nil
|
|
end
|
|
|
|
function self.cannot(event)
|
|
return not self.can(event)
|
|
end
|
|
|
|
function self.transitions()
|
|
return events_for_state[self.current]
|
|
end
|
|
|
|
function self.is_pending()
|
|
return self.confirm ~= nil
|
|
end
|
|
|
|
function self.is_finished()
|
|
return self.is(terminal)
|
|
end
|
|
|
|
if initial and not initial.defer then
|
|
self[initial_event]()
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
return M
|