app.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. -- love.run: main entrypoint function for LÖVE
  2. --
  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
  9. -- control.
  10. function love.run()
  11. Version, Major_version = App.love_version()
  12. App.snapshot_love()
  13. -- Tests always run at the start.
  14. App.run_tests_and_initialize()
  15. --? print('==')
  16. love.timer.step()
  17. local dt = 0
  18. return function()
  19. if love.event then
  20. love.event.pump()
  21. for name, a,b,c,d,e,f in love.event.poll() do
  22. if name == "quit" then
  23. if not love.quit or not love.quit() then
  24. return a or 0
  25. end
  26. end
  27. xpcall(function() love.handlers[name](a,b,c,d,e,f) end, handle_error)
  28. end
  29. end
  30. dt = love.timer.step()
  31. xpcall(function() App.update(dt) end, handle_error)
  32. love.graphics.origin()
  33. love.graphics.clear(love.graphics.getBackgroundColor())
  34. xpcall(App.draw, handle_error)
  35. love.graphics.present()
  36. love.timer.sleep(0.001)
  37. end
  38. end
  39. function handle_error(err)
  40. local callstack = debug.traceback('', --[[stack frame]]2)
  41. Error_message = 'Error: ' .. tostring(err)..'\n'..cleaned_up_callstack(callstack)
  42. print(Error_message)
  43. if Current_app == 'run' then
  44. Settings.current_app = 'source'
  45. love.filesystem.write('config', json.encode(Settings))
  46. load_file_from_source_or_save_directory('main.lua')
  47. App.undo_initialize()
  48. App.run_tests_and_initialize()
  49. else
  50. -- abort without running love.quit handler
  51. Disable_all_quit_handlers = true
  52. love.event.quit()
  53. end
  54. end
  55. -- I tend to read code from files myself (say using love.filesystem calls)
  56. -- rather than offload that to load().
  57. -- Functions compiled in this manner have ugly filenames of the form [string "filename"]
  58. -- This function cleans out this cruft from error callstacks.
  59. function cleaned_up_callstack(callstack)
  60. local frames = {}
  61. for frame in string.gmatch(callstack, '[^\n]+\n*') do
  62. local line = frame:gsub('^%s*(.-)\n?$', '%1')
  63. local filename, rest = line:match('([^:]*):(.*)')
  64. local core_filename = filename:match('^%[string "(.*)"%]$')
  65. -- pass through frames that don't match this format
  66. -- this includes the initial line "stack traceback:"
  67. local new_frame = (core_filename or filename)..':'..rest
  68. table.insert(frames, new_frame)
  69. end
  70. -- the initial "stack traceback:" line was unindented and remains so
  71. return table.concat(frames, '\n\t')
  72. end
  73. -- The rest of this file wraps around various LÖVE primitives to support
  74. -- automated tests. Often tests will run with a fake version of a primitive
  75. -- that redirects to the real love.* version once we're done with tests.
  76. --
  77. -- Not everything is so wrapped yet. Sometimes you still have to use love.*
  78. -- primitives directly.
  79. App = {}
  80. function App.love_version()
  81. local major_version, minor_version = love.getVersion()
  82. local version = major_version..'.'..minor_version
  83. return version, major_version
  84. end
  85. -- save/restore various framework globals we care about -- only on very first load
  86. function App.snapshot_love()
  87. if Love_snapshot then return end
  88. Love_snapshot = {}
  89. -- save the entire initial font; it doesn't seem reliably recreated using newFont
  90. Love_snapshot.initial_font = love.graphics.getFont()
  91. end
  92. function App.undo_initialize()
  93. love.graphics.setFont(Love_snapshot.initial_font)
  94. end
  95. function App.run_tests_and_initialize()
  96. App.load()
  97. Test_errors = {}
  98. App.run_tests()
  99. if #Test_errors > 0 then
  100. local error_message = ''
  101. if Warning_before_tests then
  102. error_message = Warning_before_tests..'\n\n'
  103. end
  104. error_message = error_message .. ('There were %d test failures:\n%s'):format(#Test_errors, table.concat(Test_errors))
  105. error(error_message)
  106. end
  107. App.disable_tests()
  108. App.initialize_globals()
  109. App.initialize(love.arg.parseGameArguments(arg), arg)
  110. end
  111. function App.run_tests()
  112. local sorted_names = {}
  113. for name,binding in pairs(_G) do
  114. if name:find('test_') == 1 then
  115. table.insert(sorted_names, name)
  116. end
  117. end
  118. table.sort(sorted_names)
  119. for _,name in ipairs(sorted_names) do
  120. App.initialize_for_test()
  121. --? print('=== '..name)
  122. --? _G[name]()
  123. xpcall(_G[name], function(err) prepend_debug_info_to_test_failure(name, err) end)
  124. end
  125. -- clean up all test methods
  126. for _,name in ipairs(sorted_names) do
  127. _G[name] = nil
  128. end
  129. end
  130. function App.initialize_for_test()
  131. App.screen.init{width=100, height=50}
  132. App.screen.contents = {} -- clear screen
  133. App.filesystem = {}
  134. App.source_dir = ''
  135. App.current_dir = ''
  136. App.save_dir = ''
  137. App.fake_keys_pressed = {}
  138. App.fake_mouse_state = {x=-1, y=-1}
  139. App.initialize_globals()
  140. end
  141. -- App.screen.resize and App.screen.move seem like better names than
  142. -- love.window.setMode and love.window.setPosition respectively. They'll
  143. -- be side-effect-free during tests, and they'll save their results in
  144. -- attributes of App.screen for easy access.
  145. App.screen={}
  146. -- Use App.screen.init in tests to initialize the fake screen.
  147. function App.screen.init(dims)
  148. App.screen.width = dims.width
  149. App.screen.height = dims.height
  150. end
  151. function App.screen.resize(width, height, flags)
  152. App.screen.width = width
  153. App.screen.height = height
  154. App.screen.flags = flags
  155. end
  156. function App.screen.size()
  157. return App.screen.width, App.screen.height, App.screen.flags
  158. end
  159. function App.screen.move(x,y, displayindex)
  160. App.screen.x = x
  161. App.screen.y = y
  162. App.screen.displayindex = displayindex
  163. end
  164. function App.screen.position()
  165. return App.screen.x, App.screen.y, App.screen.displayindex
  166. end
  167. -- If you use App.screen.print instead of love.graphics.print,
  168. -- tests will be able to check what was printed using App.screen.check below.
  169. --
  170. -- One drawback of this approach: the y coordinate used depends on font size,
  171. -- which feels brittle.
  172. function App.screen.print(msg, x,y)
  173. local screen_row = 'y'..tostring(y)
  174. --? print('drawing "'..msg..'" at y '..tostring(y))
  175. local screen = App.screen
  176. if screen.contents[screen_row] == nil then
  177. screen.contents[screen_row] = {}
  178. for i=0,screen.width-1 do
  179. screen.contents[screen_row][i] = ''
  180. end
  181. end
  182. if x < screen.width then
  183. screen.contents[screen_row][x] = msg
  184. end
  185. end
  186. function App.screen.check(y, expected_contents, msg)
  187. --? print('checking for "'..expected_contents..'" at y '..tostring(y))
  188. local screen_row = 'y'..tostring(y)
  189. local contents = ''
  190. if App.screen.contents[screen_row] == nil then
  191. error('no text at y '..tostring(y))
  192. end
  193. for i,s in ipairs(App.screen.contents[screen_row]) do
  194. contents = contents..s
  195. end
  196. check_eq(contents, expected_contents, msg)
  197. end
  198. -- If you access the time using App.get_time instead of love.timer.getTime,
  199. -- tests will be able to move the time back and forwards as needed using
  200. -- App.wait_fake_time below.
  201. App.time = 1
  202. function App.get_time()
  203. return App.time
  204. end
  205. function App.wait_fake_time(t)
  206. App.time = App.time + t
  207. end
  208. function App.width(text)
  209. return love.graphics.getFont():getWidth(text)
  210. end
  211. -- If you access the clipboard using App.get_clipboard and App.set_clipboard
  212. -- instead of love.system.getClipboardText and love.system.setClipboardText
  213. -- respectively, tests will be able to manipulate the clipboard by
  214. -- reading/writing App.clipboard.
  215. App.clipboard = ''
  216. function App.get_clipboard()
  217. return App.clipboard
  218. end
  219. function App.set_clipboard(s)
  220. App.clipboard = s
  221. end
  222. -- In tests I mostly send chords all at once to the keyboard handlers.
  223. -- However, you'll occasionally need to check if a key is down outside a handler.
  224. -- If you use App.key_down instead of love.keyboard.isDown, tests will be able to
  225. -- simulate keypresses using App.fake_key_press and App.fake_key_release
  226. -- below. This isn't very realistic, though, and it's up to tests to
  227. -- orchestrate key presses that correspond to the handlers they invoke.
  228. App.fake_keys_pressed = {}
  229. function App.key_down(key)
  230. return App.fake_keys_pressed[key]
  231. end
  232. function App.fake_key_press(key)
  233. App.fake_keys_pressed[key] = true
  234. end
  235. function App.fake_key_release(key)
  236. App.fake_keys_pressed[key] = nil
  237. end
  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.
  244. App.fake_mouse_state = {x=-1, y=-1} -- x,y always set
  245. function App.mouse_move(x,y)
  246. App.fake_mouse_state.x = x
  247. App.fake_mouse_state.y = y
  248. end
  249. function App.mouse_down(mouse_button)
  250. return App.fake_mouse_state[mouse_button]
  251. end
  252. function App.mouse_x()
  253. return App.fake_mouse_state.x
  254. end
  255. function App.mouse_y()
  256. return App.fake_mouse_state.y
  257. end
  258. function App.fake_mouse_press(x,y, mouse_button)
  259. App.fake_mouse_state.x = x
  260. App.fake_mouse_state.y = y
  261. App.fake_mouse_state[mouse_button] = true
  262. end
  263. function App.fake_mouse_release(x,y, mouse_button)
  264. App.fake_mouse_state.x = x
  265. App.fake_mouse_state.y = y
  266. App.fake_mouse_state[mouse_button] = nil
  267. end
  268. -- If you use App.open_for_reading and App.open_for_writing instead of other
  269. -- various Lua and LÖVE helpers, tests will be able to check the results of
  270. -- file operations inside the App.filesystem table.
  271. function App.open_for_reading(filename)
  272. if App.filesystem[filename] then
  273. return {
  274. lines = function(self)
  275. return App.filesystem[filename]:gmatch('[^\n]+')
  276. end,
  277. read = function(self)
  278. return App.filesystem[filename]
  279. end,
  280. close = function(self)
  281. end,
  282. }
  283. end
  284. end
  285. function App.read_file(filename)
  286. return App.filesystem[filename]
  287. end
  288. function App.open_for_writing(filename)
  289. App.filesystem[filename] = ''
  290. return {
  291. write = function(self, s)
  292. App.filesystem[filename] = App.filesystem[filename]..s
  293. end,
  294. close = function(self)
  295. end,
  296. }
  297. end
  298. function App.write_file(filename, contents)
  299. App.filesystem[filename] = contents
  300. return --[[status]] true
  301. end
  302. function App.mkdir(dirname)
  303. -- nothing in test mode
  304. end
  305. function App.remove(filename)
  306. App.filesystem[filename] = nil
  307. end
  308. -- Some helpers to trigger an event and then refresh the screen. Akin to one
  309. -- iteration of the event loop.
  310. -- all textinput events are also keypresses
  311. -- TODO: handle chords of multiple keys
  312. function App.run_after_textinput(t)
  313. App.keypressed(t)
  314. App.textinput(t)
  315. App.keyreleased(t)
  316. App.screen.contents = {}
  317. App.draw()
  318. end
  319. -- not all keys are textinput
  320. -- TODO: handle chords of multiple keys
  321. function App.run_after_keychord(chord, key)
  322. App.keychord_press(chord, key)
  323. App.keyreleased(key)
  324. App.screen.contents = {}
  325. App.draw()
  326. end
  327. function App.run_after_mouse_click(x,y, mouse_button)
  328. App.fake_mouse_press(x,y, mouse_button)
  329. App.mousepressed(x,y, mouse_button)
  330. App.fake_mouse_release(x,y, mouse_button)
  331. App.mousereleased(x,y, mouse_button)
  332. App.screen.contents = {}
  333. App.draw()
  334. end
  335. function App.run_after_mouse_press(x,y, mouse_button)
  336. App.fake_mouse_press(x,y, mouse_button)
  337. App.mousepressed(x,y, mouse_button)
  338. App.screen.contents = {}
  339. App.draw()
  340. end
  341. function App.run_after_mouse_release(x,y, mouse_button)
  342. App.fake_mouse_release(x,y, mouse_button)
  343. App.mousereleased(x,y, mouse_button)
  344. App.screen.contents = {}
  345. App.draw()
  346. end
  347. -- miscellaneous internal helpers
  348. function App.color(color)
  349. love.graphics.setColor(color.r, color.g, color.b, color.a)
  350. end
  351. -- prepend file/line/test
  352. function prepend_debug_info_to_test_failure(test_name, err)
  353. local err_without_line_number = err:gsub('^[^:]*:[^:]*: ', '')
  354. local stack_trace = debug.traceback('', --[[stack frame]]5)
  355. local file_and_line_number = stack_trace:gsub('stack traceback:\n', ''):gsub(': .*', '')
  356. local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number
  357. --? local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number..'\t\t'..stack_trace:gsub('\n', '\n\t\t')
  358. table.insert(Test_errors, full_error)
  359. end
  360. nativefs = require 'nativefs'
  361. local Keys_down = {}
  362. -- call this once all tests are run
  363. -- can't run any tests after this
  364. function App.disable_tests()
  365. -- have LÖVE delegate all handlers to App if they exist
  366. -- make sure to late-bind handlers like LÖVE's defaults do
  367. for name in pairs(love.handlers) do
  368. if App[name] then
  369. -- love.keyboard.isDown doesn't work on Android, so emulate it using
  370. -- keypressed and keyreleased events
  371. if name == 'keypressed' then
  372. love.handlers[name] = function(key, scancode, isrepeat)
  373. Keys_down[key] = true
  374. return App.keypressed(key, scancode, isrepeat)
  375. end
  376. elseif name == 'keyreleased' then
  377. love.handlers[name] = function(key, scancode)
  378. Keys_down[key] = nil
  379. return App.keyreleased(key, scancode)
  380. end
  381. else
  382. love.handlers[name] = function(...) App[name](...) end
  383. end
  384. end
  385. end
  386. -- test methods are disallowed outside tests
  387. App.run_tests = nil
  388. App.disable_tests = nil
  389. App.screen.init = nil
  390. App.filesystem = nil
  391. App.time = nil
  392. App.run_after_textinput = nil
  393. App.run_after_keychord = nil
  394. App.keypress = nil
  395. App.keyrelease = nil
  396. App.run_after_mouse_click = nil
  397. App.run_after_mouse_press = nil
  398. App.run_after_mouse_release = nil
  399. App.fake_keys_pressed = nil
  400. App.fake_key_press = nil
  401. App.fake_key_release = nil
  402. App.fake_mouse_state = nil
  403. App.fake_mouse_press = nil
  404. App.fake_mouse_release = nil
  405. -- other methods dispatch to real hardware
  406. App.screen.resize = love.window.setMode
  407. App.screen.size = love.window.getMode
  408. App.screen.move = love.window.setPosition
  409. App.screen.position = love.window.getPosition
  410. App.screen.print = love.graphics.print
  411. App.open_for_reading =
  412. function(filename)
  413. local result = nativefs.newFile(filename)
  414. local ok, err = result:open('r')
  415. if ok then
  416. return result
  417. else
  418. return ok, err
  419. end
  420. end
  421. App.read_file =
  422. function(path)
  423. if not is_absolute_path(path) then
  424. return --[[status]] false, 'Please use an unambiguous absolute path.'
  425. end
  426. local f, err = App.open_for_reading(path)
  427. if err then
  428. return --[[status]] false, err
  429. end
  430. local contents = f:read()
  431. f:close()
  432. return contents
  433. end
  434. App.open_for_writing =
  435. function(filename)
  436. local result = nativefs.newFile(filename)
  437. local ok, err = result:open('w')
  438. if ok then
  439. return result
  440. else
  441. return ok, err
  442. end
  443. end
  444. App.write_file =
  445. function(path, contents)
  446. if not is_absolute_path(path) then
  447. return --[[status]] false, 'Please use an unambiguous absolute path.'
  448. end
  449. local f, err = App.open_for_writing(path)
  450. if err then
  451. return --[[status]] false, err
  452. end
  453. f:write(contents)
  454. f:close()
  455. return --[[status]] true
  456. end
  457. App.files = nativefs.getDirectoryItems
  458. App.file_info = nativefs.getInfo
  459. App.mkdir = nativefs.createDirectory
  460. App.remove = nativefs.remove
  461. App.source_dir = love.filesystem.getSource()..'/' -- '/' should work even on Windows
  462. App.current_dir = nativefs.getWorkingDirectory()..'/'
  463. App.save_dir = love.filesystem.getSaveDirectory()..'/'
  464. App.get_time = love.timer.getTime
  465. App.get_clipboard = love.system.getClipboardText
  466. App.set_clipboard = love.system.setClipboardText
  467. App.key_down = function(key) return Keys_down[key] end
  468. App.mouse_move = love.mouse.setPosition
  469. App.mouse_down = love.mouse.isDown
  470. App.mouse_x = love.mouse.getX
  471. App.mouse_y = love.mouse.getY
  472. end