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
11 Version
, Major_version
= App
.love_version()
13 -- Tests always run at the start.
14 App
.run_tests_and_initialize()
23 for name
, a
,b
,c
,d
,e
,f
in love
.event
.poll() do
24 if name
== "quit" then
25 if not love
.quit
or not love
.quit() then
29 xpcall(function() love
.handlers
[name
](a
,b
,c
,d
,e
,f
) end, handle_error
)
33 dt
= love
.timer
.step()
34 xpcall(function() App
.update(dt
) end, handle_error
)
36 love
.graphics
.origin()
37 love
.graphics
.clear(love
.graphics
.getBackgroundColor())
38 xpcall(App
.draw
, handle_error
)
39 love
.graphics
.present()
41 love
.timer
.sleep(0.001)
45 function handle_error(err
)
46 local callstack
= debug
.traceback('', --[[stack frame]]2)
47 Error_message
= 'Error: ' .. tostring(err
)..'\n'..cleaned_up_callstack(callstack
)
49 if Current_app
== 'run' then
50 Settings
.current_app
= 'source'
51 love
.filesystem
.write('config', json
.encode(Settings
))
52 load_file_from_source_or_save_directory('main.lua')
54 App
.run_tests_and_initialize()
56 -- abort without running love.quit handler
57 Disable_all_quit_handlers
= true
62 -- I tend to read code from files myself (say using love.filesystem calls)
63 -- rather than offload that to load().
64 -- Functions compiled in this manner have ugly filenames of the form [string "filename"]
65 -- This function cleans out this cruft from error callstacks.
66 function cleaned_up_callstack(callstack
)
68 for frame
in string.gmatch(callstack
, '[^\n]+\n*') do
69 local line
= frame
:gsub('^%s*(.-)\n?$', '%1')
70 local filename
, rest
= line
:match('([^:]*):(.*)')
71 local core_filename
= filename
:match('^%[string "(.*)"%]$')
72 -- pass through frames that don't match this format
73 -- this includes the initial line "stack traceback:"
74 local new_frame
= (core_filename
or filename
)..':'..rest
75 table.insert(frames
, new_frame
)
77 -- the initial "stack traceback:" line was unindented and remains so
78 return table.concat(frames
, '\n\t')
81 -- The rest of this file wraps around various LÖVE primitives to support
82 -- automated tests. Often tests will run with a fake version of a primitive
83 -- that redirects to the real love.* version once we're done with tests.
85 -- Not everything is so wrapped yet. Sometimes you still have to use love.*
86 -- primitives directly.
90 function App
.love_version()
91 local major_version
, minor_version
= love
.getVersion()
92 local version
= major_version
..'.'..minor_version
93 return version
, major_version
96 -- save/restore various framework globals we care about -- only on very first load
97 function App
.snapshot_love()
98 if Love_snapshot
then return end
100 -- save the entire initial font; it doesn't seem reliably recreated using newFont
101 Love_snapshot
.initial_font
= love
.graphics
.getFont()
104 function App
.undo_initialize()
105 love
.graphics
.setFont(Love_snapshot
.initial_font
)
108 function App
.run_tests_and_initialize()
112 if #Test_errors
> 0 then
113 local error_message
= ''
114 if Warning_before_tests
then
115 error_message
= Warning_before_tests
..'\n\n'
117 error_message
= error_message
.. ('There were %d test failures:\n%s'):format(#Test_errors
, table.concat(Test_errors
))
121 App
.initialize_globals()
122 App
.initialize(love
.arg
.parseGameArguments(arg
), arg
)
125 function App
.run_tests()
126 local sorted_names
= {}
127 for name
,binding
in pairs(_G
) do
128 if name
:find('test_') == 1 then
129 table.insert(sorted_names
, name
)
132 table.sort(sorted_names
)
133 --? App.initialize_for_test() -- debug: run a single test at a time like these 2 lines
135 for _
,name
in ipairs(sorted_names
) do
136 App
.initialize_for_test()
137 --? print('=== '..name)
139 xpcall(_G
[name
], function(err
) prepend_debug_info_to_test_failure(name
, err
) end)
141 -- clean up all test methods
142 for _
,name
in ipairs(sorted_names
) do
147 function App
.initialize_for_test()
148 App
.screen
.init
{width
=100, height
=50}
149 App
.screen
.contents
= {} -- clear screen
154 App
.fake_keys_pressed
= {}
155 App
.fake_mouse_state
= {x
=-1, y
=-1}
156 App
.initialize_globals()
159 -- App.screen.resize and App.screen.move seem like better names than
160 -- love.window.setMode and love.window.setPosition respectively. They'll
161 -- be side-effect-free during tests, and they'll save their results in
162 -- attributes of App.screen for easy access.
166 -- Use App.screen.init in tests to initialize the fake screen.
167 function App
.screen
.init(dims
)
168 App
.screen
.width
= dims
.width
169 App
.screen
.height
= dims
.height
172 function App
.screen
.resize(width
, height
, flags
)
173 App
.screen
.width
= width
174 App
.screen
.height
= height
175 App
.screen
.flags
= flags
178 function App
.screen
.size()
179 return App
.screen
.width
, App
.screen
.height
, App
.screen
.flags
182 function App
.screen
.move(x
,y
, displayindex
)
185 App
.screen
.displayindex
= displayindex
188 function App
.screen
.position()
189 return App
.screen
.x
, App
.screen
.y
, App
.screen
.displayindex
192 -- If you use App.screen.print instead of love.graphics.print,
193 -- tests will be able to check what was printed using App.screen.check below.
195 -- One drawback of this approach: the y coordinate used depends on font size,
196 -- which feels brittle.
198 function App
.screen
.print(msg
, x
,y
)
199 local screen_row
= 'y'..tostring(y
)
200 --? print('drawing "'..msg..'" at y '..tostring(y))
201 local screen
= App
.screen
202 if screen
.contents
[screen_row
] == nil then
203 screen
.contents
[screen_row
] = {}
204 for i
=0,screen
.width
-1 do
205 screen
.contents
[screen_row
][i
] = ''
208 if x
< screen
.width
then
209 screen
.contents
[screen_row
][x
] = msg
213 function App
.screen
.check(y
, expected_contents
, msg
)
214 --? print('checking for "'..expected_contents..'" at y '..tostring(y))
215 local screen_row
= 'y'..tostring(y
)
217 if App
.screen
.contents
[screen_row
] == nil then
218 error('no text at y '..tostring(y
))
220 for i
,s
in ipairs(App
.screen
.contents
[screen_row
]) do
221 contents
= contents
..s
223 check_eq(contents
, expected_contents
, msg
)
226 -- If you access the time using App.get_time instead of love.timer.getTime,
227 -- tests will be able to move the time back and forwards as needed using
228 -- App.wait_fake_time below.
231 function App
.get_time()
234 function App
.wait_fake_time(t
)
235 App
.time
= App
.time
+ t
238 function App
.width(text
)
239 return love
.graphics
.getFont():getWidth(text
)
242 -- If you access the clipboard using App.get_clipboard and App.set_clipboard
243 -- instead of love.system.getClipboardText and love.system.setClipboardText
244 -- respectively, tests will be able to manipulate the clipboard by
245 -- reading/writing App.clipboard.
248 function App
.get_clipboard()
251 function App
.set_clipboard(s
)
255 -- In tests I mostly send chords all at once to the keyboard handlers.
256 -- However, you'll occasionally need to check if a key is down outside a handler.
257 -- If you use App.key_down instead of love.keyboard.isDown, tests will be able to
258 -- simulate keypresses using App.fake_key_press and App.fake_key_release
259 -- below. This isn't very realistic, though, and it's up to tests to
260 -- orchestrate key presses that correspond to the handlers they invoke.
262 App
.fake_keys_pressed
= {}
263 function App
.key_down(key
)
264 return App
.fake_keys_pressed
[key
]
267 function App
.fake_key_press(key
)
268 App
.fake_keys_pressed
[key
] = true
270 function App
.fake_key_release(key
)
271 App
.fake_keys_pressed
[key
] = nil
274 -- Tests mostly will invoke mouse handlers directly. However, you'll
275 -- occasionally need to check if a mouse button is down outside a handler.
276 -- If you use App.mouse_down instead of love.mouse.isDown, tests will be able to
277 -- simulate mouse clicks using App.fake_mouse_press and App.fake_mouse_release
278 -- below. This isn't very realistic, though, and it's up to tests to
279 -- orchestrate presses that correspond to the handlers they invoke.
281 App
.fake_mouse_state
= {x
=-1, y
=-1} -- x,y always set
283 function App
.mouse_move(x
,y
)
284 App
.fake_mouse_state
.x
= x
285 App
.fake_mouse_state
.y
= y
287 function App
.mouse_down(mouse_button
)
288 return App
.fake_mouse_state
[mouse_button
]
290 function App
.mouse_x()
291 return App
.fake_mouse_state
.x
293 function App
.mouse_y()
294 return App
.fake_mouse_state
.y
297 function App
.fake_mouse_press(x
,y
, mouse_button
)
298 App
.fake_mouse_state
.x
= x
299 App
.fake_mouse_state
.y
= y
300 App
.fake_mouse_state
[mouse_button
] = true
302 function App
.fake_mouse_release(x
,y
, mouse_button
)
303 App
.fake_mouse_state
.x
= x
304 App
.fake_mouse_state
.y
= y
305 App
.fake_mouse_state
[mouse_button
] = nil
308 -- If you use App.open_for_reading and App.open_for_writing instead of other
309 -- various Lua and LÖVE helpers, tests will be able to check the results of
310 -- file operations inside the App.filesystem table.
312 function App
.open_for_reading(filename
)
313 if App
.filesystem
[filename
] then
315 lines
= function(self
)
316 return App
.filesystem
[filename
]:gmatch('[^\n]+')
318 read = function(self
)
319 return App
.filesystem
[filename
]
321 close
= function(self
)
327 function App
.read_file(filename
)
328 return App
.filesystem
[filename
]
331 function App
.open_for_writing(filename
)
332 App
.filesystem
[filename
] = ''
334 write = function(self
, s
)
335 App
.filesystem
[filename
] = App
.filesystem
[filename
]..s
337 close
= function(self
)
342 function App
.write_file(filename
, contents
)
343 App
.filesystem
[filename
] = contents
344 return --[[status]] true
347 function App
.mkdir(dirname
)
348 -- nothing in test mode
351 function App
.remove(filename
)
352 App
.filesystem
[filename
] = nil
355 -- Some helpers to trigger an event and then refresh the screen. Akin to one
356 -- iteration of the event loop.
358 -- all textinput events are also keypresses
359 -- TODO: handle chords of multiple keys
360 function App
.run_after_textinput(t
)
364 App
.screen
.contents
= {}
368 -- not all keys are textinput
369 -- TODO: handle chords of multiple keys
370 function App
.run_after_keychord(chord
, key
)
371 App
.keychord_press(chord
, key
)
373 App
.screen
.contents
= {}
377 function App
.run_after_mouse_click(x
,y
, mouse_button
)
378 App
.fake_mouse_press(x
,y
, mouse_button
)
379 App
.mousepressed(x
,y
, mouse_button
)
380 App
.fake_mouse_release(x
,y
, mouse_button
)
381 App
.mousereleased(x
,y
, mouse_button
)
382 App
.screen
.contents
= {}
386 function App
.run_after_mouse_press(x
,y
, mouse_button
)
387 App
.fake_mouse_press(x
,y
, mouse_button
)
388 App
.mousepressed(x
,y
, mouse_button
)
389 App
.screen
.contents
= {}
393 function App
.run_after_mouse_release(x
,y
, mouse_button
)
394 App
.fake_mouse_release(x
,y
, mouse_button
)
395 App
.mousereleased(x
,y
, mouse_button
)
396 App
.screen
.contents
= {}
400 -- miscellaneous internal helpers
402 function App
.color(color
)
403 love
.graphics
.setColor(color
.r
, color
.g
, color
.b
, color
.a
)
406 -- prepend file/line/test
407 function prepend_debug_info_to_test_failure(test_name
, err
)
408 local err_without_line_number
= err
:gsub('^[^:]*:[^:]*: ', '')
409 local stack_trace
= debug
.traceback('', --[[stack frame]]5) -- most likely to be useful, but set to 0 for a complete stack trace
410 local file_and_line_number
= stack_trace
:gsub('stack traceback:\n', ''):gsub(': .*', '')
411 local full_error
= file_and_line_number
..':'..test_name
..' -- '..err_without_line_number
412 -- uncomment this line for a complete stack trace
413 --? local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number..'\t\t'..stack_trace:gsub('\n', '\n\t\t')
414 table.insert(Test_errors
, full_error
)
417 nativefs
= require
'nativefs'
421 -- call this once all tests are run
422 -- can't run any tests after this
423 function App
.disable_tests()
424 -- have LÖVE delegate all handlers to App if they exist
425 -- make sure to late-bind handlers like LÖVE's defaults do
426 for name
in pairs(love
.handlers
) do
428 -- love.keyboard.isDown doesn't work on Android, so emulate it using
429 -- keypressed and keyreleased events
430 if name
== 'keypressed' then
431 love
.handlers
[name
] = function(key
, scancode
, isrepeat
)
432 Keys_down
[key
] = true
433 return App
.keypressed(key
, scancode
, isrepeat
)
435 elseif name
== 'keyreleased' then
436 love
.handlers
[name
] = function(key
, scancode
)
438 return App
.keyreleased(key
, scancode
)
441 love
.handlers
[name
] = function(...) App
[name
](...) end
446 -- test methods are disallowed outside tests
448 App
.disable_tests
= nil
449 App
.screen
.init
= nil
452 App
.run_after_textinput
= nil
453 App
.run_after_keychord
= nil
456 App
.run_after_mouse_click
= nil
457 App
.run_after_mouse_press
= nil
458 App
.run_after_mouse_release
= nil
459 App
.fake_keys_pressed
= nil
460 App
.fake_key_press
= nil
461 App
.fake_key_release
= nil
462 App
.fake_mouse_state
= nil
463 App
.fake_mouse_press
= nil
464 App
.fake_mouse_release
= nil
465 -- other methods dispatch to real hardware
466 App
.screen
.resize
= love
.window
.setMode
467 App
.screen
.size
= love
.window
.getMode
468 App
.screen
.move
= love
.window
.setPosition
469 App
.screen
.position
= love
.window
.getPosition
470 App
.screen
.print = love
.graphics
.print
471 App
.open_for_reading
=
473 local result
= nativefs
.newFile(filename
)
474 local ok
, err
= result
:open('r')
483 if not is_absolute_path(path
) then
484 return --[[status]] false, 'Please use an unambiguous absolute path.'
486 local f
, err
= App
.open_for_reading(path
)
488 return --[[status]] false, err
490 local contents
= f
:read()
494 App
.open_for_writing
=
496 local result
= nativefs
.newFile(filename
)
497 local ok
, err
= result
:open('w')
505 function(path
, contents
)
506 if not is_absolute_path(path
) then
507 return --[[status]] false, 'Please use an unambiguous absolute path.'
509 local f
, err
= App
.open_for_writing(path
)
511 return --[[status]] false, err
515 return --[[status]] true
517 App
.files
= nativefs
.getDirectoryItems
518 App
.file_info
= nativefs
.getInfo
519 App
.mkdir
= nativefs
.createDirectory
520 App
.remove = nativefs
.remove
521 App
.source_dir
= love
.filesystem
.getSource()..'/' -- '/' should work even on Windows
522 App
.current_dir
= nativefs
.getWorkingDirectory()..'/'
523 App
.save_dir
= love
.filesystem
.getSaveDirectory()..'/'
524 App
.get_time
= love
.timer
.getTime
525 App
.get_clipboard
= love
.system
.getClipboardText
526 App
.set_clipboard
= love
.system
.setClipboardText
527 App
.key_down
= function(key
) return Keys_down
[key
] end
528 App
.mouse_move
= love
.mouse
.setPosition
529 App
.mouse_down
= love
.mouse
.isDown
530 App
.mouse_x
= love
.mouse
.getX
531 App
.mouse_y
= love
.mouse
.getY