1 -- love.run: main entrypoint function for LÖVE
3 -- Most apps can just use the default shown in https://love2d.org/wiki/love.run,
4 -- but we need to override it to:
5 -- * recover from errors (by switching to the source editor)
6 -- * run all tests (functions starting with 'test_') on startup, and
7 -- * save some state that makes it possible to switch between the main app
8 -- and a source editor, while giving each the illusion of complete
12 -- Tests always run at the start.
13 App
.run_tests_and_initialize()
22 for name
, a
,b
,c
,d
,e
,f
in love
.event
.poll() do
23 if name
== "quit" then
24 if not love
.quit
or not love
.quit() then
28 xpcall(function() love
.handlers
[name
](a
,b
,c
,d
,e
,f
) end, handle_error
)
32 dt
= love
.timer
.step()
33 xpcall(function() App
.update(dt
) end, handle_error
)
35 love
.graphics
.origin()
36 love
.graphics
.clear(love
.graphics
.getBackgroundColor())
37 xpcall(App
.draw
, handle_error
)
38 love
.graphics
.present()
40 love
.timer
.sleep(0.001)
44 function handle_error(err
)
45 Error_message
= debug
.traceback('Error: ' .. tostring(err
), --[[stack frame]]2):gsub('\n[^\n]+$', '')
47 if Current_app
== 'run' then
48 Settings
.current_app
= 'source'
49 love
.filesystem
.write('config', json
.encode(Settings
))
50 load_file_from_source_or_save_directory('main.lua')
52 App
.run_tests_and_initialize()
58 -- The rest of this file wraps around various LÖVE primitives to support
59 -- automated tests. Often tests will run with a fake version of a primitive
60 -- that redirects to the real love.* version once we're done with tests.
62 -- Not everything is so wrapped yet. Sometimes you still have to use love.*
63 -- primitives directly.
67 -- save/restore various framework globals we care about -- only on very first load
68 function App
.snapshot_love()
69 if Love_snapshot
then return end
71 -- save the entire initial font; it doesn't seem reliably recreated using newFont
72 Love_snapshot
.initial_font
= love
.graphics
.getFont()
75 function App
.undo_initialize()
76 love
.graphics
.setFont(Love_snapshot
.initial_font
)
79 function App
.run_tests_and_initialize()
83 if #Test_errors
> 0 then
84 error(('There were %d test failures:\n\n%s'):format(#Test_errors
, table.concat(Test_errors
)))
87 App
.initialize_globals()
88 App
.initialize(love
.arg
.parseGameArguments(arg
), arg
)
91 function App
.run_tests()
92 local sorted_names
= {}
93 for name
,binding
in pairs(_G
) do
94 if name
:find('test_') == 1 then
95 table.insert(sorted_names
, name
)
98 table.sort(sorted_names
)
99 for _
,name
in ipairs(sorted_names
) do
100 App
.initialize_for_test()
101 --? print('=== '..name)
103 xpcall(_G
[name
], function(err
) prepend_debug_info_to_test_failure(name
, err
) end)
105 -- clean up all test methods
106 for _
,name
in ipairs(sorted_names
) do
111 function App
.initialize_for_test()
112 App
.screen
.init
{width
=100, height
=50}
113 App
.screen
.contents
= {} -- clear screen
118 App
.fake_keys_pressed
= {}
119 App
.fake_mouse_state
= {x
=-1, y
=-1}
120 App
.initialize_globals()
123 -- App.screen.resize and App.screen.move seem like better names than
124 -- love.window.setMode and love.window.setPosition respectively. They'll
125 -- be side-effect-free during tests, and they'll save their results in
126 -- attributes of App.screen for easy access.
130 -- Use App.screen.init in tests to initialize the fake screen.
131 function App
.screen
.init(dims
)
132 App
.screen
.width
= dims
.width
133 App
.screen
.height
= dims
.height
136 function App
.screen
.resize(width
, height
, flags
)
137 App
.screen
.width
= width
138 App
.screen
.height
= height
139 App
.screen
.flags
= flags
142 function App
.screen
.size()
143 return App
.screen
.width
, App
.screen
.height
, App
.screen
.flags
146 function App
.screen
.move(x
,y
, displayindex
)
149 App
.screen
.displayindex
= displayindex
152 function App
.screen
.position()
153 return App
.screen
.x
, App
.screen
.y
, App
.screen
.displayindex
156 -- If you use App.screen.print instead of love.graphics.print,
157 -- tests will be able to check what was printed using App.screen.check below.
159 -- One drawback of this approach: the y coordinate used depends on font size,
160 -- which feels brittle.
162 function App
.screen
.print(msg
, x
,y
)
163 local screen_row
= 'y'..tostring(y
)
164 --? print('drawing "'..msg..'" at y '..tostring(y))
165 local screen
= App
.screen
166 if screen
.contents
[screen_row
] == nil then
167 screen
.contents
[screen_row
] = {}
168 for i
=0,screen
.width
-1 do
169 screen
.contents
[screen_row
][i
] = ''
172 if x
< screen
.width
then
173 screen
.contents
[screen_row
][x
] = msg
177 function App
.screen
.check(y
, expected_contents
, msg
)
178 --? print('checking for "'..expected_contents..'" at y '..tostring(y))
179 local screen_row
= 'y'..tostring(y
)
181 if App
.screen
.contents
[screen_row
] == nil then
182 error('no text at y '..tostring(y
))
184 for i
,s
in ipairs(App
.screen
.contents
[screen_row
]) do
185 contents
= contents
..s
187 check_eq(contents
, expected_contents
, msg
)
190 -- If you access the time using App.get_time instead of love.timer.getTime,
191 -- tests will be able to move the time back and forwards as needed using
192 -- App.wait_fake_time below.
195 function App
.get_time()
198 function App
.wait_fake_time(t
)
199 App
.time
= App
.time
+ t
202 function App
.width(text
)
203 return love
.graphics
.getFont():getWidth(text
)
206 -- If you access the clipboard using App.get_clipboard and App.set_clipboard
207 -- instead of love.system.getClipboardText and love.system.setClipboardText
208 -- respectively, tests will be able to manipulate the clipboard by
209 -- reading/writing App.clipboard.
212 function App
.get_clipboard()
215 function App
.set_clipboard(s
)
219 -- In tests I mostly send chords all at once to the keyboard handlers.
220 -- However, you'll occasionally need to check if a key is down outside a handler.
221 -- If you use App.key_down instead of love.keyboard.isDown, tests will be able to
222 -- simulate keypresses using App.fake_key_press and App.fake_key_release
223 -- below. This isn't very realistic, though, and it's up to tests to
224 -- orchestrate key presses that correspond to the handlers they invoke.
226 App
.fake_keys_pressed
= {}
227 function App
.key_down(key
)
228 return App
.fake_keys_pressed
[key
]
231 function App
.fake_key_press(key
)
232 App
.fake_keys_pressed
[key
] = true
234 function App
.fake_key_release(key
)
235 App
.fake_keys_pressed
[key
] = nil
238 -- Tests mostly will invoke mouse handlers directly. However, you'll
239 -- occasionally need to check if a mouse button is down outside a handler.
240 -- If you use App.mouse_down instead of love.mouse.isDown, tests will be able to
241 -- simulate mouse clicks using App.fake_mouse_press and App.fake_mouse_release
242 -- below. This isn't very realistic, though, and it's up to tests to
243 -- orchestrate presses that correspond to the handlers they invoke.
245 App
.fake_mouse_state
= {x
=-1, y
=-1} -- x,y always set
247 function App
.mouse_move(x
,y
)
248 App
.fake_mouse_state
.x
= x
249 App
.fake_mouse_state
.y
= y
251 function App
.mouse_down(mouse_button
)
252 return App
.fake_mouse_state
[mouse_button
]
254 function App
.mouse_x()
255 return App
.fake_mouse_state
.x
257 function App
.mouse_y()
258 return App
.fake_mouse_state
.y
261 function App
.fake_mouse_press(x
,y
, mouse_button
)
262 App
.fake_mouse_state
.x
= x
263 App
.fake_mouse_state
.y
= y
264 App
.fake_mouse_state
[mouse_button
] = true
266 function App
.fake_mouse_release(x
,y
, mouse_button
)
267 App
.fake_mouse_state
.x
= x
268 App
.fake_mouse_state
.y
= y
269 App
.fake_mouse_state
[mouse_button
] = nil
272 -- If you use App.open_for_reading and App.open_for_writing instead of other
273 -- various Lua and LÖVE helpers, tests will be able to check the results of
274 -- file operations inside the App.filesystem table.
276 function App
.open_for_reading(filename
)
277 if App
.filesystem
[filename
] then
279 lines
= function(self
)
280 return App
.filesystem
[filename
]:gmatch('[^\n]+')
282 read = function(self
)
283 return App
.filesystem
[filename
]
285 close
= function(self
)
291 function App
.read_file(filename
)
292 return App
.filesystem
[filename
]
295 function App
.open_for_writing(filename
)
296 App
.filesystem
[filename
] = ''
298 write = function(self
, s
)
299 App
.filesystem
[filename
] = App
.filesystem
[filename
]..s
301 close
= function(self
)
306 function App
.write_file(filename
, contents
)
307 App
.filesystem
[filename
] = contents
308 return --[[status]] true
311 function App
.mkdir(dirname
)
312 -- nothing in test mode
315 function App
.remove(filename
)
316 App
.filesystem
[filename
] = nil
319 -- Some helpers to trigger an event and then refresh the screen. Akin to one
320 -- iteration of the event loop.
322 -- all textinput events are also keypresses
323 -- TODO: handle chords of multiple keys
324 function App
.run_after_textinput(t
)
328 App
.screen
.contents
= {}
332 -- not all keys are textinput
333 -- TODO: handle chords of multiple keys
334 function App
.run_after_keychord(chord
)
335 App
.keychord_press(chord
)
336 App
.keyreleased(chord
)
337 App
.screen
.contents
= {}
341 function App
.run_after_mouse_click(x
,y
, mouse_button
)
342 App
.fake_mouse_press(x
,y
, mouse_button
)
343 App
.mousepressed(x
,y
, mouse_button
)
344 App
.fake_mouse_release(x
,y
, mouse_button
)
345 App
.mousereleased(x
,y
, mouse_button
)
346 App
.screen
.contents
= {}
350 function App
.run_after_mouse_press(x
,y
, mouse_button
)
351 App
.fake_mouse_press(x
,y
, mouse_button
)
352 App
.mousepressed(x
,y
, mouse_button
)
353 App
.screen
.contents
= {}
357 function App
.run_after_mouse_release(x
,y
, mouse_button
)
358 App
.fake_mouse_release(x
,y
, mouse_button
)
359 App
.mousereleased(x
,y
, mouse_button
)
360 App
.screen
.contents
= {}
364 -- miscellaneous internal helpers
366 function App
.color(color
)
367 love
.graphics
.setColor(color
.r
, color
.g
, color
.b
, color
.a
)
370 function colortable(app_color
)
371 return {app_color
.r
, app_color
.g
, app_color
.b
, app_color
.a
}
374 -- prepend file/line/test
375 function prepend_debug_info_to_test_failure(test_name
, err
)
376 local err_without_line_number
= err
:gsub('^[^:]*:[^:]*: ', '')
377 local stack_trace
= debug
.traceback('', --[[stack frame]]5)
378 local file_and_line_number
= stack_trace
:gsub('stack traceback:\n', ''):gsub(': .*', '')
379 local full_error
= file_and_line_number
..':'..test_name
..' -- '..err_without_line_number
380 --? local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number..'\t\t'..stack_trace:gsub('\n', '\n\t\t')
381 table.insert(Test_errors
, full_error
)
384 nativefs
= require
'nativefs'
388 -- call this once all tests are run
389 -- can't run any tests after this
390 function App
.disable_tests()
391 -- have LÖVE delegate all handlers to App if they exist
392 for name
in pairs(love
.handlers
) do
394 -- love.keyboard.isDown doesn't work on Android, so emulate it using
395 -- keypressed and keyreleased events
396 if name
== 'keypressed' then
397 love
.handlers
[name
] = function(key
, scancode
, isrepeat
)
398 Keys_down
[key
] = true
399 return App
.keypressed(key
, scancode
, isrepeat
)
401 elseif name
== 'keyreleased' then
402 love
.handlers
[name
] = function(key
, scancode
)
404 return App
.keyreleased(key
, scancode
)
407 love
.handlers
[name
] = App
[name
]
412 -- test methods are disallowed outside tests
414 App
.disable_tests
= nil
415 App
.screen
.init
= nil
418 App
.run_after_textinput
= nil
419 App
.run_after_keychord
= nil
422 App
.run_after_mouse_click
= nil
423 App
.run_after_mouse_press
= nil
424 App
.run_after_mouse_release
= nil
425 App
.fake_keys_pressed
= nil
426 App
.fake_key_press
= nil
427 App
.fake_key_release
= nil
428 App
.fake_mouse_state
= nil
429 App
.fake_mouse_press
= nil
430 App
.fake_mouse_release
= nil
431 -- other methods dispatch to real hardware
432 App
.screen
.resize
= love
.window
.setMode
433 App
.screen
.size
= love
.window
.getMode
434 App
.screen
.move
= love
.window
.setPosition
435 App
.screen
.position
= love
.window
.getPosition
436 App
.screen
.print = love
.graphics
.print
437 App
.open_for_reading
=
439 local result
= nativefs
.newFile(filename
)
440 local ok
, err
= result
:open('r')
449 if not is_absolute_path(path
) then
450 return --[[status]] false, 'Please use an unambiguous absolute path.'
452 local f
, err
= App
.open_for_reading(path
)
454 return --[[status]] false, err
456 local contents
= f
:read()
460 App
.open_for_writing
=
462 local result
= nativefs
.newFile(filename
)
463 local ok
, err
= result
:open('w')
471 function(path
, contents
)
472 if not is_absolute_path(path
) then
473 return --[[status]] false, 'Please use an unambiguous absolute path.'
475 local f
, err
= App
.open_for_writing(path
)
477 return --[[status]] false, err
481 return --[[status]] true
483 App
.files
= nativefs
.getDirectoryItems
484 App
.mkdir
= nativefs
.createDirectory
485 App
.remove = nativefs
.remove
486 App
.source_dir
= love
.filesystem
.getSource()..'/'
487 App
.current_dir
= nativefs
.getWorkingDirectory()..'/'
488 App
.save_dir
= love
.filesystem
.getSaveDirectory()..'/'
489 App
.get_time
= love
.timer
.getTime
490 App
.get_clipboard
= love
.system
.getClipboardText
491 App
.set_clipboard
= love
.system
.setClipboardText
492 App
.key_down
= function(key
) return Keys_down
[key
] end
493 App
.mouse_move
= love
.mouse
.setPosition
494 App
.mouse_down
= love
.mouse
.isDown
495 App
.mouse_x
= love
.mouse
.getX
496 App
.mouse_y
= love
.mouse
.getY