· 8 years ago · Mar 23, 2018, 01:42 PM
1--[[
2 SM - State Machine - 2013 Sangar
3
4 This program is licensed under the MIT license.
5 http://opensource.org/licenses/mit-license.php
6
7 This API allows it to easily set up state machines, which is frequently
8 needed when writing resumable programs.
9
10 Example use:
11 os.loadAPI("apis/state")
12 local program = state.new("/.program-state")
13 -- Add some states. Mind the colon! The first state added to the state
14 -- machine is assumed to be the entry state.
15 program:add("start", function()
16 print("Running program...")
17 stateGlobal = "This variable is available in all states."
18 -- State variables will also be saved with the state.
19 if wasRestarted == nil then
20 wasRestarted = false
21 else
22 wasRestarted = true
23 end
24 save()
25 -- If the program were to be terminated while waiting for this sleep
26 -- call to return, wasRestarted would already hold a value (false), and
27 -- be consequently set to true when the program is resumed.
28 os.sleep(5)
29 switchTo("end")
30 end)
31 program:add("end", function()
32 print("Shutting down...")
33 if wasRestarted then
34 print("This program was interrupted at least once!")
35 else
36 print("This program finished in one go.")
37 end
38 -- Setting the next state to nil will make the state machine's run
39 -- function return.
40 switchTo(nil)
41 end)
42 program:run()
43
44 This example demonstrates a couple of features:
45 - the API will wrap any callbacks with a custom environment that is shared
46 among all state functions, and that is saved when the state is saved
47 (switching to another state via switchTo() or calling save() manually).
48 - the API provides two "internal" functions to the state functions:
49 - save() which saves the program state, i.e. the currently executing
50 state function and the functions' internal environment.
51 - switchTo() which allows the functions to switch to another state
52 function. Passing nil to this function will end the state program.
53]]
54
55-------------------------------------------------------------------------------
56-------------------------------------------------------------------------------
57-- State API --
58-------------------------------------------------------------------------------
59-------------------------------------------------------------------------------
60
61-- If this API was loaded before, reuse it to avoid losing our internal state.
62if state then
63 local env = getfenv()
64 for k, v in pairs(state) do
65 env[k] = v
66 end
67 return
68end
69
70-- Namespace forward declarations.
71local class, private = {}
72
73-------------------------------------------------------------------------------
74-- Public API --
75-------------------------------------------------------------------------------
76
77-- The current version of the API.
78version = "1.2"
79
80--[[
81 Creates a new state machine which will save its state to file a the
82 specified location.
83
84 IMPORTANT: you have to ensure yourself that no two state machines with the
85 same save file are active at the same time; if they are, they will
86 overwrite each other's state files.
87
88 @param savePath the path to the file to save the state machine's state to.
89 @return the newly created state machine.
90]]
91function new(savePath)
92 assert(type(savePath) == "string" and savePath ~= "",
93 "'savePath' must be a non-empty string")
94 assert(not fs.isDir(savePath),
95 "'savePath' must not point to a folder")
96
97 -- Create a new instance.
98 local state = {}
99 local metatable = {list = {}}
100 setmetatable(state, metatable)
101 -- We do this after assigning the metatable, because otherwise the __index
102 -- will be replaced by a function wrapping the table, supposedly for
103 -- persistence if it ever makes it into CC: http://goo.gl/mS3ycx
104 -- Since we don't use any C functions this should be OK, though.
105 metatable.__index = class
106
107 -- Load an existing state, if any.
108 if not fs.exists(savePath) then
109 metatable.environment = {}
110 else
111 local file = fs.open(savePath, "r")
112 metatable.current = private.unserialize(file.readLine())
113 if type(metatable.current) ~= "string" then
114 metatable.current = nil
115 end
116 metatable.environment = private.unserialize(file.readAll())
117 if type(metatable.environment) ~= "table" then
118 metatable.environment = {}
119 end
120 file.close()
121 end
122 -- Allos us to delete the state file after the program finishes.
123 metatable.deleteState = function()
124 fs.delete(savePath)
125 end
126
127 -- Some private functions that can only be called from a state callback.
128 local function save()
129 local file = fs.open(savePath, "w")
130 file.writeLine(textutils.serialize(metatable.current))
131 file.write(textutils.serialize(metatable.environment))
132 file.close()
133 end
134 local function switchTo(name)
135 assert(name == nil or metatable.list[name], "no such state")
136 metatable.current = name
137 save()
138 end
139 local locals = {}
140 setmetatable(metatable.environment, locals)
141 -- Same trick as above, to keep it a table.
142 locals.__index = {save = save, switchTo = switchTo}
143
144 -- Done, return our state.
145 return state
146end
147
148--[[
149 Registers a new state with the specified name and callback function.
150
151 If the state is the first to be added to the state machine, it will be
152 presumed to be the entry state (i.e. it will be the first to execute when
153 the state machine is run).
154
155 @param name the name of the state to add. Other states can switch to it by
156 calling switchTo() with this value as the parameter.
157 @param callback the function to exectue when the state becomes active.
158 @return the state machine, to allow chaining.
159]]
160function class:add(name, callback)
161 local metatable = private.validate(self)
162 assert(metatable.list[name] == nil,
163 "state with name '" .. name .. "' already exists")
164 metatable.list[name] = callback
165 if metatable.entry == nil then
166 metatable.entry = name
167 end
168 return self
169end
170
171--[[
172 Run this state machine.
173
174 This will return, when an internal states switch to a nil state.
175]]
176function class:run()
177 local metatable = private.validate(self)
178 metatable.current = metatable.current or metatable.entry
179 while metatable.current do
180 local callback = metatable.list[metatable.current]
181 -- In the following we adjust the functions environment by wrapping it
182 -- in our internal one. This will make it so that when a function tries
183 -- to access a non-existing global it will write to our environment
184 -- table, which is saved.
185 -- Keep track of the original environment to allow reverting to it.
186 local oldfenv = getfenv(callback)
187 -- Adjust the environment's metatable so that the callback can still
188 -- access existing variables form its original environment.
189 setmetatable(getmetatable(metatable.environment).__index,
190 {__index = oldfenv})
191 -- Adjust the envionment and run the function.
192 setfenv(callback, metatable.environment)()
193 -- Restore the old environment (to avoid infinitely nesting of
194 -- envionments via metatables).
195 setfenv(callback, oldfenv)
196 end
197 metatable.deleteState()
198end
199
200--[[
201 Resets this state if (e.g. if it was terminated and would resume when run).
202]]
203function class:reset()
204 local metatable = private.validate(self)
205 metatable.current = nil
206 metatable.deleteState()
207end
208
209-------------------------------------------------------------------------------
210-------------------------------------------------------------------------------
211-- Internals --
212-------------------------------------------------------------------------------
213-------------------------------------------------------------------------------
214
215-- Private namespace.
216private = {}
217
218--[[
219 Checks whether the specified value is a state machine.
220
221 @return the metatable of the value.
222 @private
223]]
224function private.validate(state)
225 local metatable = getmetatable(state)
226 assert(metatable and getmetatable(state).__index == class,
227 "'state' must be a state machine instance")
228 return metatable
229end
230
231--[[
232 Custom implementation of textutils.unserialize() that properly handles
233 serialized math.huge values...
234]]
235function private.unserialize(data)
236 local result, reason = loadstring("return " .. (data or "nil"), filename)
237 if not result then
238 return data
239 else
240 return setfenv(result, {["inf"] = math.huge})()
241 end
242end