├── README.md └── modrun.lua /README.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | ## The library adds the following new features: 4 | * An FPS limiter, disabled by default 5 | * Ability to add and enable/disable event callbacks, including custom events. 6 | * Short-circuiting subsequent callbacks, if a callback or its error handler returns `true`, e.g. to capture input to UI 7 | * Maintains one copy of the module only, preventing errors should multiple libraries include their own copy 8 | 9 | ## The library adds the following new events: 10 | * **dispatch(event, ...)** - A dispatch event, to which all events are sent. **Warning**: a custom dispatch callback with priority of -5 will run before ALL other callbacks are dispatched from the default dispatch handler, e.g. before mousepressed or update callbacks with priority -20. 11 | * **pre_update(dt)** - An event that runs before love.update() 12 | * **post_update(dt)** - An event that runs after love.update() 13 | * **postprocess(draw_dt)** - A second draw event, called after draw, can be used to draw overlay, profiling info, etc. 14 | 15 | ## The library provides the following functions: 16 | * **modrun.setup()** - Sets modrun up, replacing the original love.run 17 | * **modrun.registerEventType(event, fail_if_exists)** - Registers a new event, issues a warning, or optionally an error, if the event already exists 18 | * **modrun.addCallback(event, callback, priority, self_obj, on_error)** - Registers a new callback for an event, optionally includes priority(`love.event` functions are 0, negative values will run earlier), an error handler, and an object to be passed as "self" 19 | * **on_error(err, cb, args)** - `err` is the error, `cb` the callback that produced it, `args` are arguments passed to the callback 20 | * **on_error(self_obj, err, cb, args)** - as above, but with `self_obj` passed as the `self` argument for the `function obj:fn(err, args)` format 21 | * **modrun.removeCallback(event, callback)** - Removes a previously registered callback 22 | * **modrun.enableCallback(event, callback)** - Enables a callback 23 | * **modrun.disableCallback(event, callback)** - Disables a callback, preventing it from being processed 24 | * **modrun.setFramerateLimit(fps)** - Set maximum FPS to the given value. Pass 0 or false to disable the limit. Only applies if vsync is disabled 25 | 26 | For detailed documentation of function parameters and additional comments, check source. 27 | 28 | 29 | # Usage 30 | 31 | ```lua 32 | -- To start using the library, simply do: 33 | modrun = require "modrun".setup() 34 | 35 | -- Example: Handle tweening library updates before love.update is run 36 | modrun.addCallback("update", tween.update, -1) 37 | 38 | -- Example: Implement a state handler, and handle all events using it 39 | love.state = {} 40 | modrun.addCallback("dispatch", function(event, ...) 41 | if love.state ~= nil and love.state[event] ~= nil then 42 | return love.state[event](...) 43 | end 44 | end) 45 | my_game = { update = function() end, keypressed = function() end } 46 | love.state = my_game 47 | 48 | -- Example: Add a callback with an error handler 49 | function on_error(err, cb, args) print(debug.traceback(err)) end 50 | modrun.addCallback("mousepressed", function() error("test error") end, nil, nil, on_error) 51 | 52 | -- Example: Add a callback for an object, so the object is passed as the `self` argument 53 | my_game = GameState() 54 | function my_game:update(dt) self.do_stuff(dt) end 55 | function my_game:on_error(err, cb, args) self.do_other_stuff(err) end 56 | -- Without error handling 57 | modrun.addCallback("update", GameState.update, nil, GameState) 58 | -- Or with error handling 59 | modrun.addCallback("update", GameState.update, nil, GameState, my_game.on_error) 60 | 61 | -- Example: Don't process further callbacks after returning true: 62 | -- Prevent quitting while the game is saving 63 | modrun.addCallback("quit", function() return is_game_saving() end, -99) 64 | -- Don't propagate events if an UI element was clicked 65 | function ui_handler(x, y, button, ...) 66 | if ui_element_clicked(x, y) return true end 67 | end 68 | modrun.addCallback("mousepressed", ui_handler, -99) 69 | ``` 70 | 71 | 72 | # Future directions 73 | 74 | - [ ] Run dispatch inbetween hooks, so that dispatch with priority -5 dispatches mouspressed AFTER a mousepressed callback with priority -10, rather than before like now. Maybe as a separate event type to support both behaviors? 75 | - [ ] Add table-of-callbacks operation for stuff like e.g. `modrun.addMultipleCallbacks(my_library.callbacks)` without having to manually add each. 76 | - [ ] Add an event that handles uncaught errors in other events(`uncaughterror` maybe?) in situations where not crashing the entire game is desired. -------------------------------------------------------------------------------- /modrun.lua: -------------------------------------------------------------------------------- 1 | --Copyright (c) 2015 - 2023 Llamageddon 2 | -- 3 | --Permission is hereby granted, free of charge, to any person obtaining a 4 | --copy of this software and associated documentation files (the 5 | --"Software"), to deal in the Software without restriction, including 6 | --without limitation the rights to use, copy, modify, merge, publish, 7 | --distribute, sublicense, and/or sell copies of the Software, and to 8 | --permit persons to whom the Software is furnished to do so, subject to 9 | --the following conditions: 10 | -- 11 | --The above copyright notice and this permission notice shall be included 12 | --in all copies or substantial portions of the Software. 13 | -- 14 | --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | --OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | --MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | --IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | --CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | --TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | --SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -- Only one instance of modrun can exist, if it is to function right 23 | if __modrun_singleton then 24 | return __modrun_singleton 25 | end 26 | 27 | local modrun = {} 28 | 29 | local function error_check(condition, message, level) 30 | return condition or error(message, (level or 1) + 1) 31 | end 32 | 33 | local noop = function() end 34 | 35 | modrun.deltatime = 0 36 | modrun.max_fps = 0 37 | 38 | -- Extend love.handlers with new(pre, post, etc.) and previously-inlined(load,update,draw) functions 39 | love.handlers = love.handlers or {} 40 | 41 | love.handlers.pre_quit = noop 42 | love.handlers.pre_update = noop 43 | love.handlers.post_update = noop 44 | love.handlers.postprocess = noop 45 | 46 | love.handlers.load = function(...) 47 | if love.load then love.load(...) end 48 | end 49 | love.handlers.update = function(...) 50 | if love.update then love.update(...) end 51 | end 52 | love.handlers.draw = function() 53 | if love.draw then love.draw() end 54 | end 55 | 56 | -- Dispatches events to base handlers, and calls callbacks 57 | -- @param event - The name of the event for which the dispatch is being handled 58 | -- @param ... - Arguments associated with the event 59 | -- @returns `true` if any of the event handlers has returned `true`, `false` otherwise 60 | 61 | -- Dispatches the initial dispatch event for an event, which runs the handlers for both dispatch and the event itself 62 | function modrun._dispatchEvent(event, ...) 63 | love.handlers.dispatch("dispatch", event, ...) 64 | end 65 | 66 | function love.handlers.dispatch(event, ...) 67 | for _, cb_entry in ipairs(modrun.sorted_enabled_callbacks[event] or {}) do 68 | -- local cb, err_handler, self_obj, enabled, priority = cb_entry[1], cb_entry[2], cb_entry[3], cb_entry[4], cb_entry[5] 69 | local cb, enabled, priority, err_handler, self_obj = cb_entry[1], cb_entry[2], cb_entry[3], cb_entry[4], cb_entry[5] 70 | local abort 71 | if not enabled then 72 | -- Pass, do nothing 73 | elseif err_handler == nil then 74 | -- If no error handler was provided, we just let the error propagate down the stack 75 | if self_obj then abort = cb(self_obj, ...) else abort = cb(...) end 76 | else 77 | -- If an error handler was passed, handle any potential errors via it 78 | local success, result 79 | if self_obj ~= nil then 80 | success, result = xpcall(cb, function(err) return err end, self_obj, ...) 81 | abort = result 82 | if not success then abort = err_handler(self_obj, result, cb, {...}) end 83 | else 84 | success, result = xpcall(cb, function(err) return err end, ...) 85 | abort = result 86 | if not success then abort = err_handler(result, cb, {...}) end 87 | end 88 | end 89 | if abort then return true end -- Short-circuit and cancel calling the rest of callbacks 90 | end 91 | 92 | return false 93 | end 94 | -- Each entry has the format of: { callback, enabled, priority, on_error, self_obj } 95 | modrun.callbacks = {} 96 | -- Sorted arrays of enabled callbacks for a given event 97 | modrun.sorted_enabled_callbacks = {} 98 | 99 | -- Register a new event type 100 | -- @param event - The name of the event to be registered 101 | -- @param fail_if_exists - (Optional) Throw an error if the event already exists 102 | function modrun.registerEventType(event, fail_if_exists) 103 | if not love.handlers[event] then 104 | love.handlers[event] = noop 105 | modrun.callbacks[event] = modrun.callbacks[event] or {} 106 | else 107 | if fail_if_exists then 108 | error("[Error] Event type '" .. event .. "' is already registered.") 109 | else 110 | print("[Warning] Event type '" .. event .. "' is already registered.") 111 | end 112 | end 113 | end 114 | 115 | -- Behind-the-scenes function that performs callback operations, in order to have shared logic in one place. 116 | local callback_actions = {add = true, remove = true, enable = true, disable = true} 117 | function modrun._setCallback(action, event, callback, priority, self_obj, on_error) 118 | error_check(callback_actions[action], "Unknown action('" .. action .. "') specified. Must be one of: add, remove, enable, disable.", 2) 119 | error_check(event and rawget(love.handlers, event), "Unknown or invalid event type has been provided: '" .. tostring(event) .. "'", 2) 120 | if action == "add" then 121 | error_check(callback and type(callback) == "function", "No or invalid callback function has been provided: '" .. tostring(callback) .. "'", 2) 122 | modrun.callbacks[event] = modrun.callbacks[event] or {} 123 | modrun.callbacks[event][callback] = {callback, true, priority or 0, on_error, self_obj} 124 | else 125 | error_check(callback and modrun.callbacks[event][callback], "Unregistered or invalid callback has been provided", 2) 126 | if action == "remove" then 127 | modrun.callbacks[event][callback] = nil 128 | elseif action == "enable" then 129 | modrun.callbacks[event][callback][2] = true 130 | elseif action == "disable" then 131 | modrun.callbacks[event][callback][2] = false 132 | end 133 | end 134 | 135 | -- Re-sort the enabled callbacks table for the given event 136 | modrun.sorted_enabled_callbacks[event] = {} 137 | for _, cb_entry in pairs(modrun.callbacks[event] or {}) do 138 | if cb_entry[2] then -- If it's enabled 139 | table.insert(modrun.sorted_enabled_callbacks[event], cb_entry) 140 | end 141 | end 142 | table.sort(modrun.sorted_enabled_callbacks[event], function(a, b) return a[3] < b[3] end) 143 | end 144 | 145 | -- Add a callback function to be called on event 146 | -- If the callback returns true, further callbacks will be blocked from running 147 | -- @param event - The name of the event for which a callback is being registered 148 | -- @param callback - The callback function 149 | -- @param priority - Defines the order in which callbacks are executed, starting with lowest values 150 | -- @param self_obj - (Optional) A value to pass as the first "self" parameter to the handler 151 | -- @param on_error - (Optional) Error handler to invoke when the event handler throws an error 152 | -- The following parameters are passed to the handler: 153 | -- * self_obj, if one was provided 154 | -- * Event info, in format of {event, ...} 155 | -- * The error traceback 156 | function modrun.addCallback(event, callback, priority, self_obj, on_error) 157 | modrun._setCallback("add", event, callback, priority, self_obj, on_error) 158 | end 159 | 160 | -- Remove a callback 161 | -- @param event - The name of the event for which a callback is being removed 162 | -- @param callback - The callback function that was originally provided to modrun.addCallback 163 | function modrun.removeCallback(event, callback) 164 | modrun._setCallback("remove", event, callback) 165 | end 166 | 167 | -- Enable a callback 168 | -- @param event - The name of the event for which a callback is being enabled 169 | -- @param callback - The callback function that was originally provided to modrun.addCallback 170 | function modrun.enableCallback(event, callback) 171 | modrun._setCallback("enable", event, callback) 172 | end 173 | 174 | -- Disable a callback 175 | -- @param event - The name of the event for which a callback is being disabled 176 | -- @param callback - The callback function that was originally provided to modrun.addCallback 177 | function modrun.disableCallback(event, callback) 178 | modrun._setCallback("disable", event, callback) 179 | end 180 | 181 | -- Set framerate limit 182 | -- @param fps - Desired number of frames per second 183 | function modrun.setFramerateLimit(fps) 184 | modrun.max_fps = fps or 0 185 | end 186 | 187 | -- A simple function ran when love has been terminated 188 | function modrun._shutdown() 189 | if love.audio then 190 | love.audio.stop() 191 | end 192 | end 193 | 194 | -- The run function to replace `love.run` 195 | function modrun.run() 196 | for event, handler in pairs(love.handlers) do 197 | modrun.addCallback(event, handler, 0) 198 | end 199 | 200 | if love.event then love.event.pump() end 201 | ---@diagnostic disable-next-line: undefined-field 202 | modrun._dispatchEvent("load", love.arg.parseGameArguments(arg), arg) 203 | -- We don't want the first frame's dt to include time taken by love.load. 204 | if love.timer then love.timer.step() end 205 | 206 | modrun.deltatime = 0 207 | -- Main loop time. 208 | return function() -- Love2D wants a function it can call continuously rather than for love.run() to run a loop itself 209 | -- Process events. 210 | if love.event then 211 | love.event.pump() 212 | for event, a, b, c, d, e, f in love.event.poll() do 213 | -- Quit has to be handled as a special case 214 | if event == "quit" then 215 | local abort_quit = modrun._dispatchEvent(event, a, b, c, d, e, f) 216 | if not abort_quit then modrun._shutdown(); return a or 0 end 217 | else 218 | -- The rest of events can be handled normally 219 | modrun._dispatchEvent(event,a,b,c,d,e,f) -- Does not include update or draw 220 | end 221 | end 222 | end 223 | 224 | -- Update dt, as we'll be passing it to update 225 | if love.timer then modrun.deltatime = love.timer.step() end 226 | 227 | local before_update = love.timer.getTime() 228 | 229 | -- Call update and draw 230 | modrun._dispatchEvent("pre_update", modrun.deltatime) -- will pass 0 if love.timer is disabled 231 | modrun._dispatchEvent("update", modrun.deltatime) -- will pass 0 if love.timer is disabled 232 | modrun._dispatchEvent("post_update", modrun.deltatime) -- will pass 0 if love.timer is disabled 233 | 234 | if love.graphics and love.graphics.isActive() then 235 | love.graphics.clear(love.graphics.getBackgroundColor()) 236 | love.graphics.origin() 237 | local start = love.timer.getTime() 238 | modrun._dispatchEvent("draw") 239 | modrun._dispatchEvent("postprocess", love.timer.getTime() - start) 240 | love.graphics.present() 241 | end 242 | 243 | if love.timer then love.timer.sleep(0.001) end 244 | 245 | -- If vsync is disabled, and FPS limit is enabled, enforce it 246 | local w, h, flags = love.window.getMode() 247 | if not flags.vsync and modrun.max_fps > 0 then 248 | local max_delta, delta = (1 / modrun.max_fps), love.timer.getTime() - before_update 249 | if delta < max_delta then love.timer.sleep(max_delta - delta) end 250 | end 251 | end 252 | end 253 | 254 | function modrun.setup() 255 | love.run = modrun.run 256 | return modrun 257 | end 258 | 259 | modrun._DESCRIPTION = 'An alternative run function module for Love2D with support for callbacks and additional events' 260 | modrun._VERSION = 'modrun v2.0.2' 261 | modrun._URL = 'http://github.com/Asmageddon/modrun' 262 | modrun._LICENSE = 'MIT LICENSE ' 263 | 264 | __modrun_singleton = modrun 265 | 266 | return modrun --------------------------------------------------------------------------------