app.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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. --? App.initialize_for_test() -- debug: run a single test at a time like these 2 lines
  120. --? test_search()
  121. for _,name in ipairs(sorted_names) do
  122. App.initialize_for_test()
  123. --? print('=== '..name)
  124. --? _G[name]()
  125. xpcall(_G[name], function(err) prepend_debug_info_to_test_failure(name, err) end)
  126. end
  127. -- clean up all test methods
  128. for _,name in ipairs(sorted_names) do
  129. _G[name] = nil
  130. end
  131. end
  132. function App.initialize_for_test()
  133. App.screen.init{width=100, height=50}
  134. App.screen.contents = {} -- clear screen
  135. App.filesystem = {}
  136. App.source_dir = ''
  137. App.current_dir = ''
  138. App.save_dir = ''
  139. App.fake_keys_pressed = {}
  140. App.fake_mouse_state = {x=-1, y=-1}
  141. App.initialize_globals()
  142. end
  143. -- App.screen.resize and App.screen.move seem like better names than
  144. -- love.window.setMode and love.window.setPosition respectively. They'll
  145. -- be side-effect-free during tests, and they'll save their results in
  146. -- attributes of App.screen for easy access.
  147. App.screen={}
  148. -- Use App.screen.init in tests to initialize the fake screen.
  149. function App.screen.init(dims)
  150. App.screen.width = dims.width
  151. App.screen.height = dims.height
  152. end
  153. function App.screen.resize(width, height, flags)
  154. App.screen.width = width
  155. App.screen.height = height
  156. App.screen.flags = flags
  157. end
  158. function App.screen.size()
  159. return App.screen.width, App.screen.height, App.screen.flags
  160. end
  161. function App.screen.move(x,y, displayindex)
  162. App.screen.x = x
  163. App.screen.y = y
  164. App.screen.displayindex = displayindex
  165. end
  166. function App.screen.position()
  167. return App.screen.x, App.screen.y, App.screen.displayindex
  168. end
  169. -- If you use App.screen.print instead of love.graphics.print,
  170. -- tests will be able to check what was printed using App.screen.check below.
  171. --
  172. -- One drawback of this approach: the y coordinate used depends on font size,
  173. -- which feels brittle.
  174. function App.screen.print(msg, x,y)
  175. local screen_row = 'y'..tostring(y)
  176. --? print('drawing "'..msg..'" at y '..tostring(y))
  177. local screen = App.screen
  178. if screen.contents[screen_row] == nil then
  179. screen.contents[screen_row] = {}
  180. for i=0,screen.width-1 do
  181. screen.contents[screen_row][i] = ''
  182. end
  183. end
  184. if x < screen.width then
  185. screen.contents[screen_row][x] = msg
  186. end
  187. end
  188. function App.screen.check(y, expected_contents, msg)
  189. --? print('checking for "'..expected_contents..'" at y '..tostring(y))
  190. local screen_row = 'y'..tostring(y)
  191. local contents = ''
  192. if App.screen.contents[screen_row] == nil then
  193. error('no text at y '..tostring(y))
  194. end
  195. for i,s in ipairs(App.screen.contents[screen_row]) do
  196. contents = contents..s
  197. end
  198. check_eq(contents, expected_contents, msg)
  199. end
  200. -- If you access the time using App.get_time instead of love.timer.getTime,
  201. -- tests will be able to move the time back and forwards as needed using
  202. -- App.wait_fake_time below.
  203. App.time = 1
  204. function App.get_time()
  205. return App.time
  206. end
  207. function App.wait_fake_time(t)
  208. App.time = App.time + t
  209. end
  210. function App.width(text)
  211. return love.graphics.getFont():getWidth(text)
  212. end
  213. -- If you access the clipboard using App.get_clipboard and App.set_clipboard
  214. -- instead of love.system.getClipboardText and love.system.setClipboardText
  215. -- respectively, tests will be able to manipulate the clipboard by
  216. -- reading/writing App.clipboard.
  217. App.clipboard = ''
  218. function App.get_clipboard()
  219. return App.clipboard
  220. end
  221. function App.set_clipboard(s)
  222. App.clipboard = s
  223. end
  224. -- In tests I mostly send chords all at once to the keyboard handlers.
  225. -- However, you'll occasionally need to check if a key is down outside a handler.
  226. -- If you use App.key_down instead of love.keyboard.isDown, tests will be able to
  227. -- simulate keypresses using App.fake_key_press and App.fake_key_release
  228. -- below. This isn't very realistic, though, and it's up to tests to
  229. -- orchestrate key presses that correspond to the handlers they invoke.
  230. App.fake_keys_pressed = {}
  231. function App.key_down(key)
  232. return App.fake_keys_pressed[key]
  233. end
  234. function App.fake_key_press(key)
  235. App.fake_keys_pressed[key] = true
  236. end
  237. function App.fake_key_release(key)
  238. App.fake_keys_pressed[key] = nil
  239. end
  240. -- Tests mostly will invoke mouse handlers directly. However, you'll
  241. -- occasionally need to check if a mouse button is down outside a handler.
  242. -- If you use App.mouse_down instead of love.mouse.isDown, tests will be able to
  243. -- simulate mouse clicks using App.fake_mouse_press and App.fake_mouse_release
  244. -- below. This isn't very realistic, though, and it's up to tests to
  245. -- orchestrate presses that correspond to the handlers they invoke.
  246. 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
  250. end
  251. function App.mouse_down(mouse_button)
  252. return App.fake_mouse_state[mouse_button]
  253. end
  254. function App.mouse_x()
  255. return App.fake_mouse_state.x
  256. end
  257. function App.mouse_y()
  258. return App.fake_mouse_state.y
  259. end
  260. function App.fake_mouse_press(x,y, mouse_button)
  261. App.fake_mouse_state.x = x
  262. App.fake_mouse_state.y = y
  263. App.fake_mouse_state[mouse_button] = true
  264. end
  265. function App.fake_mouse_release(x,y, mouse_button)
  266. App.fake_mouse_state.x = x
  267. App.fake_mouse_state.y = y
  268. App.fake_mouse_state[mouse_button] = nil
  269. end
  270. -- If you use App.open_for_reading and App.open_for_writing instead of other
  271. -- various Lua and LÖVE helpers, tests will be able to check the results of
  272. -- file operations inside the App.filesystem table.
  273. function App.open_for_reading(filename)
  274. if App.filesystem[filename] then
  275. return {
  276. lines = function(self)
  277. return App.filesystem[filename]:gmatch('[^\n]+')
  278. end,
  279. read = function(self)
  280. return App.filesystem[filename]
  281. end,
  282. close = function(self)
  283. end,
  284. }
  285. end
  286. end
  287. function App.read_file(filename)
  288. return App.filesystem[filename]
  289. end
  290. function App.open_for_writing(filename)
  291. App.filesystem[filename] = ''
  292. return {
  293. write = function(self, s)
  294. App.filesystem[filename] = App.filesystem[filename]..s
  295. end,
  296. close = function(self)
  297. end,
  298. }
  299. end
  300. function App.write_file(filename, contents)
  301. App.filesystem[filename] = contents
  302. return --[[status]] true
  303. end
  304. function App.mkdir(dirname)
  305. -- nothing in test mode
  306. end
  307. function App.remove(filename)
  308. App.filesystem[filename] = nil
  309. end
  310. -- Some helpers to trigger an event and then refresh the screen. Akin to one
  311. -- iteration of the event loop.
  312. -- all textinput events are also keypresses
  313. -- TODO: handle chords of multiple keys
  314. function App.run_after_textinput(t)
  315. App.keypressed(t)
  316. App.textinput(t)
  317. App.keyreleased(t)
  318. App.screen.contents = {}
  319. App.draw()
  320. end
  321. -- not all keys are textinput
  322. -- TODO: handle chords of multiple keys
  323. function App.run_after_keychord(chord, key)
  324. App.keychord_press(chord, key)
  325. App.keyreleased(key)
  326. App.screen.contents = {}
  327. App.draw()
  328. end
  329. function App.run_after_mouse_click(x,y, mouse_button)
  330. App.fake_mouse_press(x,y, mouse_button)
  331. App.mousepressed(x,y, mouse_button)
  332. App.fake_mouse_release(x,y, mouse_button)
  333. App.mousereleased(x,y, mouse_button)
  334. App.screen.contents = {}
  335. App.draw()
  336. end
  337. function App.run_after_mouse_press(x,y, mouse_button)
  338. App.fake_mouse_press(x,y, mouse_button)
  339. App.mousepressed(x,y, mouse_button)
  340. App.screen.contents = {}
  341. App.draw()
  342. end
  343. function App.run_after_mouse_release(x,y, mouse_button)
  344. App.fake_mouse_release(x,y, mouse_button)
  345. App.mousereleased(x,y, mouse_button)
  346. App.screen.contents = {}
  347. App.draw()
  348. end
  349. -- miscellaneous internal helpers
  350. function App.color(color)
  351. love.graphics.setColor(color.r, color.g, color.b, color.a)
  352. end
  353. -- prepend file/line/test
  354. function prepend_debug_info_to_test_failure(test_name, err)
  355. local err_without_line_number = err:gsub('^[^:]*:[^:]*: ', '')
  356. local stack_trace = debug.traceback('', --[[stack frame]]5) -- most likely to be useful, but set to 0 for a complete stack trace
  357. local file_and_line_number = stack_trace:gsub('stack traceback:\n', ''):gsub(': .*', '')
  358. local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number
  359. -- uncomment this line for a complete stack trace
  360. --? local full_error = file_and_line_number..':'..test_name..' -- '..err_without_line_number..'\t\t'..stack_trace:gsub('\n', '\n\t\t')
  361. table.insert(Test_errors, full_error)
  362. end
  363. nativefs = require 'nativefs'
  364. local Keys_down = {}
  365. -- call this once all tests are run
  366. -- can't run any tests after this
  367. function App.disable_tests()
  368. -- have LÖVE delegate all handlers to App if they exist
  369. -- make sure to late-bind handlers like LÖVE's defaults do
  370. for name in pairs(love.handlers) do
  371. if App[name] then
  372. -- love.keyboard.isDown doesn't work on Android, so emulate it using
  373. -- keypressed and keyreleased events
  374. if name == 'keypressed' then
  375. love.handlers[name] = function(key, scancode, isrepeat)
  376. Keys_down[key] = true
  377. return App.keypressed(key, scancode, isrepeat)
  378. end
  379. elseif name == 'keyreleased' then
  380. love.handlers[name] = function(key, scancode)
  381. Keys_down[key] = nil
  382. return App.keyreleased(key, scancode)
  383. end
  384. else
  385. love.handlers[name] = function(...) App[name](...) end
  386. end
  387. end
  388. end
  389. -- test methods are disallowed outside tests
  390. App.run_tests = nil
  391. App.disable_tests = nil
  392. App.screen.init = nil
  393. App.filesystem = nil
  394. App.time = nil
  395. App.run_after_textinput = nil
  396. App.run_after_keychord = nil
  397. App.keypress = nil
  398. App.keyrelease = nil
  399. App.run_after_mouse_click = nil
  400. App.run_after_mouse_press = nil
  401. App.run_after_mouse_release = nil
  402. App.fake_keys_pressed = nil
  403. App.fake_key_press = nil
  404. App.fake_key_release = nil
  405. App.fake_mouse_state = nil
  406. App.fake_mouse_press = nil
  407. App.fake_mouse_release = nil
  408. -- other methods dispatch to real hardware
  409. App.screen.resize = love.window.setMode
  410. App.screen.size = love.window.getMode
  411. App.screen.move = love.window.setPosition
  412. App.screen.position = love.window.getPosition
  413. App.screen.print = love.graphics.print
  414. App.open_for_reading =
  415. function(filename)
  416. local result = nativefs.newFile(filename)
  417. local ok, err = result:open('r')
  418. if ok then
  419. return result
  420. else
  421. return ok, err
  422. end
  423. end
  424. App.read_file =
  425. function(path)
  426. if not is_absolute_path(path) then
  427. return --[[status]] false, 'Please use an unambiguous absolute path.'
  428. end
  429. local f, err = App.open_for_reading(path)
  430. if err then
  431. return --[[status]] false, err
  432. end
  433. local contents = f:read()
  434. f:close()
  435. return contents
  436. end
  437. App.open_for_writing =
  438. function(filename)
  439. local result = nativefs.newFile(filename)
  440. local ok, err = result:open('w')
  441. if ok then
  442. return result
  443. else
  444. return ok, err
  445. end
  446. end
  447. App.write_file =
  448. function(path, contents)
  449. if not is_absolute_path(path) then
  450. return --[[status]] false, 'Please use an unambiguous absolute path.'
  451. end
  452. local f, err = App.open_for_writing(path)
  453. if err then
  454. return --[[status]] false, err
  455. end
  456. f:write(contents)
  457. f:close()
  458. return --[[status]] true
  459. end
  460. App.files = nativefs.getDirectoryItems
  461. App.file_info = nativefs.getInfo
  462. App.mkdir = nativefs.createDirectory
  463. App.remove = nativefs.remove
  464. App.source_dir = love.filesystem.getSource()..'/' -- '/' should work even on Windows
  465. App.current_dir = nativefs.getWorkingDirectory()..'/'
  466. App.save_dir = love.filesystem.getSaveDirectory()..'/'
  467. App.get_time = love.timer.getTime
  468. App.get_clipboard = love.system.getClipboardText
  469. App.set_clipboard = love.system.setClipboardText
  470. App.key_down = function(key) return Keys_down[key] end
  471. App.mouse_move = love.mouse.setPosition
  472. App.mouse_down = love.mouse.isDown
  473. App.mouse_x = love.mouse.getX
  474. App.mouse_y = love.mouse.getY
  475. end