├── .github └── workflows │ ├── luacheck.yml │ └── testy.yml ├── .luacheckrc ├── .test └── tests.lua ├── assert.lua ├── async.lua ├── class.lua ├── colour.lua ├── functional.lua ├── identifier.lua ├── init.lua ├── intersect.lua ├── license.txt ├── make_pooled.lua ├── manual_gc.lua ├── mathx.lua ├── measure.lua ├── pathfind.lua ├── pretty.lua ├── pubsub.lua ├── readme.md ├── sequence.lua ├── set.lua ├── sort.lua ├── state_machine.lua ├── stringx.lua ├── tablex.lua ├── timer.lua ├── vec2.lua └── vec3.lua /.github/workflows/luacheck.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | luacheck: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | with: 13 | # Subdirectory to avoid linting luarocks code. Use same dir as testy. 14 | path: batteries 15 | - name: Setup Lua 16 | uses: leafo/gh-actions-lua@v10 17 | with: 18 | luaVersion: 5.4 19 | - name: Setup Lua Rocks 20 | uses: leafo/gh-actions-luarocks@v4 21 | - name: Setup luacheck 22 | run: luarocks install luacheck 23 | - name: Run Code Linter 24 | run: | 25 | cd batteries 26 | luacheck . 27 | -------------------------------------------------------------------------------- /.github/workflows/testy.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | testy: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | with: 13 | # tests.lua expects top level folder to be 'batteries' 14 | path: 'batteries' 15 | - name: Setup Lua 16 | uses: leafo/gh-actions-lua@v10 17 | with: 18 | luaVersion: 5.4 19 | - name: Setup Lua Rocks 20 | uses: leafo/gh-actions-luarocks@v4 21 | - name: Setup testy 22 | # Install from github because rock is 6+ years old. 23 | run: luarocks install https://raw.githubusercontent.com/siffiejoe/lua-testy/master/testy-scm-0.rockspec 24 | - name: Run tests 25 | run: | 26 | cd batteries 27 | testy.lua .test/tests.lua 28 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | return { 2 | std = "lua51+love", 3 | ignore = { 4 | "211", -- Unused local variable. 5 | "212/self", -- Unused argument self. 6 | "213", -- Unused loop variable. 7 | "214", -- used _ prefix variable (we use this often to indicate private variables, not unused) 8 | "631", -- Line is too long. 9 | }, 10 | files = { 11 | ["tests.lua"] = { 12 | ignore = { 13 | "211", -- Unused local variable. (testy will find these local functions) 14 | }, 15 | }, 16 | ["assert.lua"] = { 17 | ignore = { 18 | "121", -- Setting a read-only global variable. (we clobber assert) 19 | }, 20 | }, 21 | ["init.lua"] = { 22 | ignore = { 23 | "111", -- Setting an undefined global variable. (batteries and ripairs) 24 | "121", -- Setting a read-only global variable. (we clobber assert) 25 | "143", -- Accessing an undefined field of a global variable. (we use tablex as table) 26 | }, 27 | }, 28 | ["sort.lua"] = { 29 | ignore = { 30 | "142", -- Setting an undefined field of a global variable. (inside export) 31 | }, 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.test/tests.lua: -------------------------------------------------------------------------------- 1 | -- Run this file with testy from within batteries 2 | -- testy.lua .tests/tests.lua 3 | -- testy sets `...` to "module.test", so ignore that and use module top-level paths. 4 | package.path = package.path .. ";../?.lua" 5 | 6 | local assert = require("batteries.assert") 7 | local tablex = require("batteries.tablex") 8 | local identifier = require("batteries.identifier") 9 | local stringx = require("batteries.stringx") 10 | 11 | -- tablex {{{ 12 | 13 | local function test_shallow_copy() 14 | local x,r 15 | x = { a = 1, b = 2, c = 3 } 16 | r = tablex.shallow_copy(x) 17 | assert:equal(r.a, 1) 18 | assert:equal(r.b, 2) 19 | assert:equal(r.c, 3) 20 | 21 | x = { a = { b = { 2 }, c = { 3 }, } } 22 | r = tablex.shallow_copy(x) 23 | assert:equal(r.a, x.a) 24 | end 25 | 26 | local function test_deep_copy() 27 | local x,r 28 | x = { a = 1, b = 2, c = 3 } 29 | r = tablex.deep_copy(x) 30 | assert:equal(r.a, 1) 31 | assert:equal(r.b, 2) 32 | assert:equal(r.c, 3) 33 | 34 | x = { a = { b = { 2 }, c = { 3 }, } } 35 | r = tablex.deep_copy(x) 36 | assert(r.a ~= x.a) 37 | assert:equal(r.a.b[1], 2) 38 | assert:equal(r.a.c[1], 3) 39 | end 40 | 41 | 42 | local function test_shallow_overlay() 43 | local x,y,r 44 | x = { a = 1, b = 2, c = 3 } 45 | y = { c = 8, d = 9 } 46 | r = tablex.shallow_overlay(x, y) 47 | assert( 48 | tablex.deep_equal( 49 | r, 50 | { a = 1, b = 2, c = 8, d = 9 } 51 | ) 52 | ) 53 | 54 | x = { b = { 2 }, c = { 3 }, } 55 | y = { c = { 8 }, d = { 9 }, } 56 | r = tablex.shallow_overlay(x, y) 57 | assert(r.b == x.b) 58 | assert(r.c == y.c) 59 | assert(r.d == y.d) 60 | assert( 61 | tablex.deep_equal( 62 | r, 63 | { b = { 2 }, c = { 8 }, d = { 9 }, } 64 | ) 65 | ) 66 | end 67 | 68 | local function test_deep_overlay() 69 | local x,y,r 70 | x = { a = 1, b = 2, c = 3 } 71 | y = { c = 8, d = 9 } 72 | r = tablex.deep_overlay(x, y) 73 | assert( 74 | tablex.deep_equal( 75 | r, 76 | { a = 1, b = 2, c = 8, d = 9 } 77 | ) 78 | ) 79 | 80 | x = { a = { b = { 2 }, c = { 3 }, } } 81 | y = { a = { c = { 8 }, d = { 9 }, } } 82 | r = tablex.deep_overlay(x, y) 83 | assert( 84 | tablex.deep_equal( 85 | r, 86 | { a = { b = { 2 }, c = { 8 }, d = { 9 }, } } 87 | ) 88 | ) 89 | end 90 | 91 | 92 | local function test_shallow_equal() 93 | local x,y 94 | x = { a = { b = { 2 }, } } 95 | y = { a = { b = { 2 }, } } 96 | assert(not tablex.shallow_equal(x, y)) 97 | 98 | x = { 3, 4, "hello", [20] = "end", } 99 | y = { 3, 4, "hello", [20] = "end", } 100 | assert(tablex.shallow_equal(x, y)) 101 | 102 | local z = { 1, 2, } 103 | x = { a = z, b = 10, c = true, } 104 | y = { a = z, b = 10, c = true, } 105 | assert(tablex.shallow_equal(x, y)) 106 | assert(tablex.shallow_equal(y, x)) 107 | end 108 | 109 | local function test_deep_equal() 110 | local x,y 111 | x = { a = { b = { 2 }, c = { 3 }, } } 112 | y = { a = { b = { 2 }, c = { 3 }, } } 113 | assert(tablex.deep_equal(x, y)) 114 | 115 | x = { a = { b = { 1, 2 }, c = { 3 }, } } 116 | y = { a = { c = { 3 }, b = { [2] = 2, [1] = 1 }, } } 117 | assert(tablex.deep_equal(x, y)) 118 | assert(tablex.deep_equal(y, x)) 119 | 120 | x = { a = { b = { 2 }, c = { 3 }, 2 } } 121 | y = { a = { b = { 2 }, c = { 3 }, } } 122 | assert(not tablex.deep_equal(x, y)) 123 | assert(not tablex.deep_equal(y, x)) 124 | end 125 | 126 | local function test_spairs() 127 | local t = { 128 | player1 = { 129 | name = "Joe", 130 | score = 8 131 | }, 132 | player2 = { 133 | name = "Robert", 134 | score = 7 135 | }, 136 | player3 = { 137 | name = "John", 138 | score = 10 139 | } 140 | } 141 | 142 | local sorted_names = {} 143 | local sorted_score = {} 144 | 145 | for k, v in tablex.spairs(t, function(a, b) 146 | return t[a].score > t[b].score 147 | end) do 148 | tablex.push(sorted_names, v.name) 149 | tablex.push(sorted_score, v.score) 150 | end 151 | 152 | assert(tablex.deep_equal(sorted_names, { 153 | "John", "Joe", "Robert" 154 | })) 155 | 156 | assert(tablex.deep_equal(sorted_score, { 157 | 10, 8, 7 158 | })) 159 | end 160 | 161 | local function test_uuid4() 162 | for i = 1, 5 do 163 | local id = identifier.uuid4() 164 | 165 | -- right len 166 | assert(#id == 36) 167 | -- right amount of non hyphen characters 168 | assert(#id:gsub("-", "") == 32) 169 | 170 | -- 15th char is always a 4 171 | assert(id:sub(15, 15) == "4") 172 | -- 20th char is always between 0x8 and 0xb 173 | local y = tonumber("0x" .. id:sub(20, 20)) 174 | assert(y >= 0x8 and y <= 0xb) 175 | 176 | -- everything is a valid 8 bit num 177 | for char in id:gsub("-", ""):gmatch(".") do 178 | local num = assert(tonumber("0x" .. char)) 179 | assert(num >= 0 and num <= 0xf) 180 | end 181 | end 182 | end 183 | 184 | local function test_ulid() 185 | -- bail if there's no appropriate time func 186 | if select(2, pcall(identifier.ulid)):find('time function') then return end 187 | 188 | for i = 1, 5 do 189 | local ulid = assert(identifier.ulid()) 190 | 191 | -- right len 192 | assert(#ulid == 26) 193 | -- have the same timestamp with the same time 194 | local a, b = identifier.ulid(nil, 1):sub(1, 10), identifier.ulid(nil, 1):sub(1, 10) 195 | assert(a == b) 196 | -- don't have characters out of crockford base32 197 | assert(not ulid:match("[ILOU%l]")) 198 | end 199 | end 200 | 201 | -- stringx 202 | local function test_title_case() 203 | local str = "the quick brown fox jumps over the lazy dog" 204 | 205 | assert(stringx.title_case(str) == "The Quick Brown Fox Jumps Over The Lazy Dog") 206 | end 207 | -------------------------------------------------------------------------------- /assert.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | various intuitive assertions that 3 | - avoid garbage generation upon success, 4 | - build nice formatted error messages 5 | - post one level above the call site by default 6 | - return their first argument so they can be used inline 7 | 8 | default call is builtin global assert 9 | 10 | can call nop() to dummy out everything for "release mode" 11 | (if you're worried about that sort of thing) 12 | ]] 13 | 14 | local _assert = assert 15 | 16 | --proxy calls to global assert 17 | local assert = setmetatable({}, { 18 | __call = function(self, ...) 19 | return _assert(...) 20 | end, 21 | }) 22 | 23 | local function _extra(msg) 24 | if not msg then 25 | return "" 26 | end 27 | return "\n\n\t(note: " .. msg .. ")" 28 | end 29 | 30 | --assert a value is not nil 31 | --return the value, so this can be chained 32 | function assert:some(v, msg, stack_level) 33 | if v == nil then 34 | error(("assertion failed: value is nil %s"):format( 35 | _extra(msg) 36 | ), 2 + (stack_level or 0)) 37 | end 38 | return v 39 | end 40 | 41 | --assert two values are equal 42 | function assert:equal(a, b, msg, stack_level) 43 | if a ~= b then 44 | error(("assertion failed: %s is not equal to %s %s"):format( 45 | tostring(a), 46 | tostring(b), 47 | _extra(msg) 48 | ), 2 + (stack_level or 0)) 49 | end 50 | return a 51 | end 52 | 53 | --assert two values are not equal 54 | function assert:not_equal(a, b, msg, stack_level) 55 | if a == b then 56 | error(("assertion failed: values are equal %s"):format( 57 | _extra(msg) 58 | ), 2 + (stack_level or 0)) 59 | end 60 | return a 61 | end 62 | 63 | --assert a value is of a certain type 64 | function assert:type(a, t, msg, stack_level) 65 | if type(a) ~= t then 66 | error(("assertion failed: %s (%s) not of type %s %s"):format( 67 | tostring(a), 68 | type(a), 69 | tostring(t), 70 | _extra(msg) 71 | ), 2 + (stack_level or 0)) 72 | end 73 | return a 74 | end 75 | 76 | --assert a value is nil or a certain type. 77 | -- useful for optional parameters. 78 | function assert:type_or_nil(a, t, msg, stack_level) 79 | if a ~= nil then 80 | assert:type(a, t, msg, stack_level + 1) 81 | end 82 | return a 83 | end 84 | 85 | --assert a value is one of those in a table of options 86 | function assert:one_of(a, t, msg, stack_level) 87 | for _, value in ipairs(t) do 88 | if value == a then 89 | return a 90 | end 91 | end 92 | 93 | local values = {} 94 | for index = 1, #t do 95 | values[index] = tostring(t[index]) 96 | end 97 | 98 | error(("assertion failed: %s not one of %s %s"):format( 99 | tostring(a), 100 | table.concat(values, ", "), 101 | _extra(msg) 102 | ), 2 + (stack_level or 0)) 103 | end 104 | 105 | --replace everything in assert with nop functions that just return their second argument, for near-zero overhead on release 106 | function assert:nop() 107 | local nop = function(_, a) 108 | return a 109 | end 110 | setmetatable(self, { 111 | __call = nop, 112 | }) 113 | for k, v in pairs(self) do 114 | self[k] = nop 115 | end 116 | end 117 | 118 | return assert 119 | -------------------------------------------------------------------------------- /async.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | simple kernel for async tasks running in the background 3 | 4 | can "stall" a task by yielding the string "stall" 5 | this will suspend the coroutine until the rest of 6 | the queue has been processed or stalled 7 | and can early-out update_for_time 8 | 9 | todo: 10 | multiple types of callbacks 11 | finish, error, step 12 | getting a reference to the task for manipulation 13 | attaching multiple callbacks 14 | cancelling 15 | proper error traces for coroutines with async:add, additional wrapper? 16 | ]] 17 | 18 | local path = (...):gsub("async", "") 19 | local assert = require(path .. "assert") 20 | local class = require(path .. "class") 21 | local tablex = require(path .. "tablex") 22 | 23 | local async = class({ 24 | name = "async", 25 | }) 26 | 27 | function async:new() 28 | self.tasks = {} 29 | self.tasks_stalled = {} 30 | end 31 | 32 | local capture_callstacks 33 | if love and love.system and love.system.getOS() == 'Web' then 34 | --do no extra wrapping under lovejs because using xpcall 35 | -- causes a yield across a c call boundary 36 | capture_callstacks = function(f) 37 | return f 38 | end 39 | else 40 | capture_callstacks = function(f) 41 | --report errors with the coroutine's callstack instead of one coming 42 | -- from async:update 43 | return function(...) 44 | local results = {xpcall(f, debug.traceback, ...)} 45 | local success = table.remove(results, 1) 46 | if not success then 47 | error(table.remove(results, 1)) 48 | end 49 | return unpack(results) 50 | end 51 | end 52 | end 53 | 54 | --add a task to the kernel 55 | function async:call(f, args, callback, error_callback) 56 | assert:type_or_nil(args, "table", "async:call - args", 1) 57 | f = capture_callstacks(f) 58 | return self:add(coroutine.create(f), args, callback, error_callback) 59 | end 60 | 61 | --add an already-existing coroutine to the kernel 62 | function async:add(co, args, callback, error_callback) 63 | local task = { 64 | co, 65 | args or {}, 66 | callback or false, 67 | error_callback or false, 68 | } 69 | table.insert(self.tasks, task) 70 | return task 71 | end 72 | 73 | --remove a running task based on the reference we got earlier 74 | function async:remove(task) 75 | task.remove = true 76 | if coroutine.status(task[1]) == "running" then 77 | --removed the current running task 78 | return true 79 | else 80 | --remove from the queues 81 | return tablex.remove_value(self.tasks, task) 82 | or tablex.remove_value(self.tasks_stalled, task) 83 | end 84 | end 85 | 86 | --separate local for processing a resume; 87 | -- because the results come as varargs this way 88 | local function process_resume(self, task, success, msg, ...) 89 | local co, args, cb, error_cb = unpack(task) 90 | --error? 91 | if not success then 92 | if error_cb then 93 | error_cb(msg) 94 | else 95 | local err = ("failure in async task:\n\n\t%s\n") 96 | :format(tostring(msg)) 97 | error(err) 98 | end 99 | end 100 | --check done 101 | if coroutine.status(co) == "dead" or task.remove then 102 | --done? run callback with result 103 | if cb then 104 | cb(msg, ...) 105 | end 106 | else 107 | --if not completed, re-add to the appropriate queue 108 | if msg == "stall" then 109 | --add to stalled queue as signalled stall 110 | table.insert(self.tasks_stalled, task) 111 | else 112 | table.insert(self.tasks, task) 113 | end 114 | end 115 | end 116 | 117 | --update some task in the kernel 118 | function async:update() 119 | --grab task definition 120 | local task = table.remove(self.tasks, 1) 121 | if not task then 122 | --have we got stalled tasks to re-try? 123 | if #self.tasks_stalled > 0 then 124 | --swap queues rather than churning elements 125 | self.tasks_stalled, self.tasks = self.tasks, self.tasks_stalled 126 | return self:update() 127 | else 128 | return false 129 | end 130 | end 131 | 132 | --run a step 133 | --(using unpack because coroutine is also nyi and it's core to this async model) 134 | local co, args = unpack(task) 135 | process_resume(self, task, coroutine.resume(co, unpack(args))) 136 | 137 | return true 138 | end 139 | 140 | --update tasks for some amount of time 141 | function async:update_for_time(t, early_out_stalls) 142 | local now = love.timer.getTime() 143 | while love.timer.getTime() - now < t do 144 | if not self:update() then 145 | break 146 | end 147 | --all stalled? 148 | if early_out_stalls and #self.tasks == 0 then 149 | break 150 | end 151 | end 152 | end 153 | 154 | --add a function to run after a certain delay (in seconds) 155 | function async:add_timeout(f, delay) 156 | self:call(function() 157 | async.wait(delay) 158 | f() 159 | end) 160 | end 161 | 162 | --add a function to run repeatedly every delay (in seconds) 163 | --note: not super useful currently unless you plan to destroy the whole async kernel 164 | -- as there's no way to remove tasks :) 165 | function async:add_interval(f, delay) 166 | self:call(function() 167 | while true do 168 | async.wait(delay) 169 | f() 170 | end 171 | end) 172 | end 173 | 174 | --await the result of a function or set of functions 175 | --return the results 176 | function async:await(to_call, args) 177 | local single_call = false 178 | if type(to_call) == "function" then 179 | to_call = {to_call} 180 | single_call = true 181 | end 182 | 183 | local awaiting = #to_call 184 | local results = {} 185 | for i, v in ipairs(to_call) do 186 | self:call(function(...) 187 | table.insert(results, {v(...)}) 188 | awaiting = awaiting - 1 189 | end, args) 190 | end 191 | 192 | while awaiting > 0 do 193 | async.stall() 194 | end 195 | 196 | --unwrap 197 | if single_call then 198 | results = results[1] 199 | end 200 | 201 | return results 202 | end 203 | 204 | --static async operation helpers 205 | -- these are not methods on the async object, but are 206 | -- intended to be called with dot syntax on the class itself 207 | 208 | --stall the current coroutine 209 | function async.stall() 210 | return coroutine.yield("stall") 211 | end 212 | 213 | --make the current coroutine wait 214 | function async.wait(time) 215 | if not coroutine.running() then 216 | error("attempt to wait in main thread, this will block forever") 217 | end 218 | local now = love.timer.getTime() 219 | while love.timer.getTime() - now < time do 220 | async.stall() 221 | end 222 | end 223 | 224 | --eventually get a result, inline 225 | -- repeatedly calls the provided function until it returns something, 226 | -- stalling each time it doesn't, returning the result in the end 227 | function async.value(f) 228 | local r = f() 229 | while not r do 230 | async.stall() 231 | r = f() 232 | end 233 | return r 234 | end 235 | 236 | --make an iterator or search function asynchronous, stalling every n (or 1) iterations 237 | --can be useful with functional queries as well, if they are done in a coroutine. 238 | function async.wrap_iterator(f, stall, n) 239 | stall = stall or false 240 | n = n or 1 241 | local count = 0 242 | return function(...) 243 | count = count + 1 244 | if count >= n then 245 | count = 0 246 | if stall then 247 | async.stall() 248 | else 249 | coroutine.yield() 250 | end 251 | end 252 | return f(...) 253 | end 254 | end 255 | return async 256 | -------------------------------------------------------------------------------- /class.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | barebones oop basics 3 | 4 | construction 5 | 6 | call the class object to construct a new instance 7 | 8 | this will construct a new table, assign it as a class 9 | instance, and call `new` 10 | 11 | if you are defining a subclass, you will need to call 12 | `self:super(...)` as part of `new` to complete superclass 13 | construction - if done correctly this will propagate 14 | up the chain and you wont have to think about it 15 | 16 | classes are used as metatables directly so that 17 | metamethods "just work" - except for index, which is 18 | used to hook up instance methods 19 | 20 | classes do use a prototype chain for inheritance, but 21 | also copy their interfaces (including superclass) 22 | 23 | we copy interfaces in classes rather than relying on 24 | a prototype chain, so that pairs on the class gets 25 | all the methods when implemented as an interface 26 | 27 | class properties are not copied and should likely 28 | be accessed through the concrete class object so 29 | that everything refers to the same object 30 | 31 | arguments (all optional): 32 | name (string): 33 | the name to use for type() 34 | extends (class): 35 | superclass for basic inheritance 36 | implements (ordered table of classes): 37 | mixins/interfaces 38 | default_tostring (boolean): 39 | whether or not to provide a default tostring function 40 | 41 | ]] 42 | 43 | --generate unique increasing class ids 44 | local class_id_gen = 0 45 | local function next_class_id() 46 | class_id_gen = class_id_gen + 1 47 | return class_id_gen 48 | end 49 | 50 | --implement an interface into c 51 | local function implement(c, interface) 52 | c.__is[interface] = true 53 | for k, v in pairs(interface) do 54 | if c[k] == nil and type(v) == "function" then 55 | c[k] = v 56 | end 57 | end 58 | end 59 | 60 | --build a new class 61 | local function class(config) 62 | local class_id = next_class_id() 63 | 64 | config = config or {} 65 | local extends = config.extends 66 | local implements = config.implements 67 | local src_location = "call location not available" 68 | if debug and debug.getinfo then 69 | local dinfo = debug.getinfo(2) 70 | local src = dinfo.short_src 71 | local line = dinfo.currentline 72 | src_location = ("%s:%d"):format(src, line) 73 | end 74 | local name = config.name or ("unnamed class %d (%s)"):format( 75 | class_id, 76 | src_location 77 | ) 78 | 79 | local c = {} 80 | 81 | --prototype 82 | c.__index = c 83 | 84 | --unique generated id per-class 85 | c.__id = class_id 86 | 87 | --the class name for type calls 88 | c.__type = name 89 | 90 | --return the name of the class 91 | function c:type() 92 | return self.__type 93 | end 94 | 95 | if config.default_tostring then 96 | function c:__tostring() 97 | return name 98 | end 99 | end 100 | 101 | --class metatable to set up constructor call 102 | setmetatable(c, { 103 | __call = function(self, ...) 104 | local instance = setmetatable({}, self) 105 | instance:new(...) 106 | return instance 107 | end, 108 | __index = extends, 109 | }) 110 | 111 | --checking class membership for probably-too-dynamic code 112 | --returns true for both extended classes and implemented interfaces 113 | --(implemented with a hashset for fast lookups) 114 | c.__is = {} 115 | c.__is[c] = true 116 | function c:is(t) 117 | return self.__is[t] == true 118 | end 119 | 120 | --get the inherited class for super calls if/as needed 121 | --allows overrides that still refer to superclass behaviour 122 | c.__super = extends 123 | 124 | --perform a (partial) super construction for an instance 125 | --for any nested super calls, it'll call the relevant one in the 126 | --heirarchy, assuming no super calls have been missed 127 | function c:super(...) 128 | if not c.__super then return end 129 | --hold reference so we can restore 130 | local current_super = c.__super 131 | --push next super 132 | c.__super = c.__super.__super 133 | --call 134 | current_super.new(self, ...) 135 | --restore 136 | c.__super = current_super 137 | end 138 | 139 | 140 | if c.__super then 141 | --implement superclass interface 142 | implement(c, c.__super) 143 | end 144 | 145 | 146 | --implement all the passed interfaces/mixins 147 | --in order provided 148 | if implements then 149 | for _, interface in ipairs(implements) do 150 | implement(c, interface) 151 | end 152 | end 153 | 154 | --default constructor, just proxy to the super constructor 155 | --override it and use to set up the properties of the instance 156 | --but don't forget to call the super constructor! 157 | function c:new(...) 158 | self:super(...) 159 | end 160 | 161 | --done 162 | return c 163 | end 164 | 165 | return class 166 | -------------------------------------------------------------------------------- /colour.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | colour handling stuff 3 | ]] 4 | 5 | local path = (...):gsub("colour", "") 6 | 7 | 8 | local math = require(path.."mathx") 9 | 10 | local colour = {} 11 | 12 | ------------------------------------------------------------------------------- 13 | -- hex handling routines 14 | -- pack and unpack into 24 or 32 bit hex numbers 15 | 16 | local ok, bit = pcall(require, "bit") 17 | if ok then 18 | --we have bit operations module, use the fast path 19 | local band, bor = bit.band, bit.bor 20 | local lshift, rshift = bit.lshift, bit.rshift 21 | 22 | --rgb only (no alpha) 23 | function colour.pack_rgb(r, g, b) 24 | local br = lshift(band(0xff, r * 255), 16) 25 | local bg = lshift(band(0xff, g * 255), 8) 26 | local bb = lshift(band(0xff, b * 255), 0) 27 | return bor( br, bg, bb ) 28 | end 29 | 30 | function colour.unpack_rgb(rgb) 31 | local r = rshift(band(rgb, 0x00ff0000), 16) / 255 32 | local g = rshift(band(rgb, 0x0000ff00), 8) / 255 33 | local b = rshift(band(rgb, 0x000000ff), 0) / 255 34 | return r, g, b 35 | end 36 | 37 | --argb format (common for shared hex) 38 | 39 | function colour.pack_argb(r, g, b, a) 40 | local ba = lshift(band(0xff, a * 255), 24) 41 | local br = lshift(band(0xff, r * 255), 16) 42 | local bg = lshift(band(0xff, g * 255), 8) 43 | local bb = lshift(band(0xff, b * 255), 0) 44 | return bor( br, bg, bb, ba ) 45 | end 46 | 47 | function colour.unpack_argb(argb) 48 | local r = rshift(band(argb, 0x00ff0000), 16) / 255 49 | local g = rshift(band(argb, 0x0000ff00), 8) / 255 50 | local b = rshift(band(argb, 0x000000ff), 0) / 255 51 | local a = rshift(band(argb, 0xff000000), 24) / 255 52 | return r, g, b, a 53 | end 54 | 55 | --rgba format 56 | function colour.pack_rgba(r, g, b, a) 57 | local br = lshift(band(0xff, r * 255), 24) 58 | local bg = lshift(band(0xff, g * 255), 16) 59 | local bb = lshift(band(0xff, b * 255), 8) 60 | local ba = lshift(band(0xff, a * 255), 0) 61 | return bor( br, bg, bb, ba ) 62 | end 63 | 64 | function colour.unpack_rgba(rgba) 65 | local r = rshift(band(rgba, 0xff000000), 24) / 255 66 | local g = rshift(band(rgba, 0x00ff0000), 16) / 255 67 | local b = rshift(band(rgba, 0x0000ff00), 8) / 255 68 | local a = rshift(band(rgba, 0x000000ff), 0) / 255 69 | return r, g, b, a 70 | end 71 | else 72 | --we don't have bitops, use a slower pure-float path 73 | local floor = math.floor 74 | 75 | --rgb only (no alpha) 76 | function colour.pack_rgb(r, g, b) 77 | local br = floor(0xff * r) % 0x100 * 0x10000 78 | local bg = floor(0xff * g) % 0x100 * 0x100 79 | local bb = floor(0xff * b) % 0x100 80 | return br + bg + bb 81 | end 82 | 83 | function colour.unpack_rgb(rgb) 84 | local r = floor(rgb / 0x10000) % 0x100 85 | local g = floor(rgb / 0x100) % 0x100 86 | local b = floor(rgb) % 0x100 87 | return r / 255, g / 255, b / 255 88 | end 89 | 90 | --argb format (common for shared hex) 91 | function colour.pack_argb(r, g, b, a) 92 | local ba = floor(0xff * a) % 0x100 * 0x1000000 93 | return colour.pack_rgb(r, g, b) + ba 94 | end 95 | 96 | function colour.unpack_argb(argb) 97 | local r, g, b = colour.unpack_rgb(argb) 98 | local a = floor(argb / 0x1000000) % 0x100 99 | return r, g, b, a / 255 100 | end 101 | 102 | --rgba format 103 | function colour.pack_rgba(r, g, b, a) 104 | local ba = floor(0xff * a) % 0x100 105 | return colour.pack_rgb(r, g, b) * 0x100 + ba 106 | end 107 | 108 | function colour.unpack_rgba(rgba) 109 | local r, g, b = colour.unpack_rgb(floor(rgba / 0x100)) 110 | local a = floor(rgba) % 0x100 111 | return r, g, b, a 112 | end 113 | end 114 | 115 | 116 | ------------------------------------------------------------------------------- 117 | -- colour space conversion 118 | -- rgb is the common language for computers 119 | -- but it's useful to have other spaces to work in for us as humans :) 120 | 121 | --convert hsl to rgb 122 | --all components are 0-1, hue is fraction of a turn rather than degrees or radians 123 | function colour.hsl_to_rgb(h, s, l) 124 | --wedge slice 125 | local w = (math.wrap(h, 0, 1) * 6) 126 | --chroma 127 | local c = (1 - math.abs(2 * l - 1)) * s 128 | --secondary 129 | local x = c * (1 - math.abs(w % 2 - 1)) 130 | --lightness boost 131 | local m = l - c / 2 132 | --per-wedge logic 133 | local r, g, b = m, m, m 134 | if w < 1 then 135 | r = r + c 136 | g = g + x 137 | elseif w < 2 then 138 | r = r + x 139 | g = g + c 140 | elseif w < 3 then 141 | g = g + c 142 | b = b + x 143 | elseif w < 4 then 144 | g = g + x 145 | b = b + c 146 | elseif w < 5 then 147 | b = b + c 148 | r = r + x 149 | else 150 | b = b + x 151 | r = r + c 152 | end 153 | return r, g, b 154 | end 155 | 156 | --convert rgb to hsl 157 | function colour.rgb_to_hsl(r, g, b) 158 | local max, min = math.max(r, g, b), math.min(r, g, b) 159 | if max == min then return 0, 0, min end 160 | 161 | local l, d = max + min, max - min 162 | local s = d / (l > 1 and (2 - l) or l) 163 | l = l / 2 164 | local h --depends on below 165 | if max == r then 166 | h = (g - b) / d 167 | if g < b then h = h + 6 end 168 | elseif max == g then 169 | h = (b - r) / d + 2 170 | else 171 | h = (r - g) / d + 4 172 | end 173 | return h / 6, s, l 174 | end 175 | 176 | --convert hsv to rgb 177 | --all components are 0-1, hue is fraction of a turn rather than degrees or radians 178 | function colour.hsv_to_rgb(h, s, v) 179 | --wedge slice 180 | local w = (math.wrap(h, 0, 1) * 6) 181 | --chroma 182 | local c = v * s 183 | --secondary 184 | local x = c * (1 - math.abs(w % 2 - 1)) 185 | --match value 186 | local m = v - c 187 | --per-wedge logic 188 | local r, g, b = m, m, m 189 | if w < 1 then 190 | r = r + c 191 | g = g + x 192 | elseif w < 2 then 193 | r = r + x 194 | g = g + c 195 | elseif w < 3 then 196 | g = g + c 197 | b = b + x 198 | elseif w < 4 then 199 | g = g + x 200 | b = b + c 201 | elseif w < 5 then 202 | b = b + c 203 | r = r + x 204 | else 205 | b = b + x 206 | r = r + c 207 | end 208 | return r, g, b 209 | end 210 | 211 | --convert rgb to hsv 212 | function colour.rgb_to_hsv(r, g, b) 213 | local max, min = math.max(r, g, b), math.min(r, g, b) 214 | if max == min then return 0, 0, min end 215 | local v, d = max, max - min 216 | local s = (max == 0) and 0 or (d / max) 217 | local h --depends on below 218 | if max == r then 219 | h = (g - b) / d 220 | if g < b then h = h + 6 end 221 | elseif max == g then 222 | h = (b - r) / d + 2 223 | else 224 | h = (r - g) / d + 4 225 | end 226 | return h / 6, s, v 227 | end 228 | 229 | --conversion between hsl and hsv 230 | function colour.hsl_to_hsv(h, s, l) 231 | local v = l + s * math.min(l, 1 - l) 232 | s = (v == 0) and 0 or (2 * (1 - l / v)) 233 | return h, s, v 234 | end 235 | 236 | function colour.hsv_to_hsl(h, s, v) 237 | local l = v * (1 - s / 2) 238 | s = (l == 0 or l == 1) and 0 or ((v - l) / math.min(l, 1 - l)) 239 | return h, s, l 240 | end 241 | 242 | --oklab https://bottosson.github.io/posts/oklab/ 243 | function colour.oklab_to_rgb(l, a, b) 244 | local _l = l + 0.3963377774 * a + 0.2158037573 * b 245 | local _m = l - 0.1055613458 * a - 0.0638541728 * b 246 | local _s = l - 0.0894841775 * a - 1.2914855480 * b 247 | 248 | _l = math.pow(_l, 3.0) 249 | _m = math.pow(_m, 3.0) 250 | _s = math.pow(_s, 3.0) 251 | 252 | local red, green, blue = love.math.linearToGamma( 253 | ( 4.0767245293 * _l - 3.3072168827 * _m + 0.2307590544 * _s), 254 | (-1.2681437731 * _l + 2.6093323231 * _m - 0.3411344290 * _s), 255 | (-0.0041119885 * _l - 0.7034763098 * _m + 1.7068625689 * _s) 256 | ) 257 | return red, green, blue 258 | end 259 | 260 | function colour.rgb_to_oklab(red, green, blue) 261 | red, green, blue = love.math.gammaToLinear(red, green, blue) 262 | 263 | local _l = 0.4121656120 * red + 0.5362752080 * green + 0.0514575653 * blue 264 | local _m = 0.2118591070 * red + 0.6807189584 * green + 0.1074065790 * blue 265 | local _s = 0.0883097947 * red + 0.2818474174 * green + 0.6302613616 * blue 266 | 267 | _l = math.pow(_l, 1.0 / 3.0) 268 | _m = math.pow(_m, 1.0 / 3.0) 269 | _s = math.pow(_s, 1.0 / 3.0) 270 | 271 | local l = 0.2104542553 * _l + 0.7936177850 * _m - 0.0040720468 * _s 272 | local a = 1.9779984951 * _l - 2.4285922050 * _m + 0.4505937099 * _s 273 | local b = 0.0259040371 * _l + 0.7827717662 * _m - 0.8086757660 * _s 274 | 275 | return l, a, b 276 | end 277 | 278 | --colour distance functions 279 | --distance of one colour to another (linear space) 280 | --can be used for finding nearest colours for palette mapping, for example 281 | 282 | function colour.distance_rgb( 283 | ar, ag, ab, 284 | br, bg, bb 285 | ) 286 | local dr, dg, db = ar - br, ag - bg, ab - bb 287 | return math.sqrt(dr * dr + dg * dg + db * db) 288 | end 289 | 290 | function colour.distance_packed_rgb(a, b) 291 | local ar, ag, ab = colour.unpack_rgb(a) 292 | local br, bg, bb = colour.unpack_rgb(b) 293 | return colour.distance_rgb( 294 | ar, ag, ab, 295 | br, bg, bb 296 | ) 297 | end 298 | 299 | --todo: rgba and various other unpacks 300 | 301 | return colour 302 | -------------------------------------------------------------------------------- /functional.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | functional programming facilities 3 | 4 | be wary of use in performance critical code under luajit 5 | 6 | absolute performance is this module's achilles heel; 7 | you're generally allocating more garbage than is strictly necessary, 8 | plus inline anonymous will be re-created each call, which is NYI 9 | 10 | this can be a Bad Thing and means probably this isn't a great module 11 | to heavily leverage in the middle of your action game's physics update 12 | 13 | but, there are many cases where it matters less than you'd think 14 | generally, if it wasn't hot enough to get compiled anyway, you're fine 15 | 16 | (if all this means nothing to you, just don't worry about it) 17 | ]] 18 | 19 | local path = (...):gsub("functional", "") 20 | local tablex = require(path .. "tablex") 21 | local mathx = require(path .. "mathx") 22 | 23 | local functional = setmetatable({}, { 24 | __index = tablex, 25 | }) 26 | 27 | --the identity function 28 | function functional.identity(v) 29 | return v 30 | end 31 | 32 | --simple sequential iteration, f is called for all elements of t 33 | --f can return non-nil to break the loop (and return the value) 34 | --otherwise returns t for chaining 35 | function functional.foreach(t, f) 36 | for i = 1, #t do 37 | local result = f(t[i], i) 38 | if result ~= nil then 39 | return result 40 | end 41 | end 42 | return t 43 | end 44 | 45 | --performs a left to right reduction of t using f, with seed as the initial value 46 | -- reduce({1, 2, 3}, 0, f) -> f(f(f(0, 1), 2), 3) 47 | -- (but performed iteratively, so no stack smashing) 48 | function functional.reduce(t, seed, f) 49 | for i = 1, #t do 50 | seed = f(seed, t[i], i) 51 | end 52 | return seed 53 | end 54 | 55 | --maps a sequence {a, b, c} -> {f(a), f(b), f(c)} 56 | -- (automatically drops any nils to keep a sequence, so can be used to simultaneously map and filter) 57 | function functional.map(t, f) 58 | local result = {} 59 | for i = 1, #t do 60 | local v = f(t[i], i) 61 | if v ~= nil then 62 | table.insert(result, v) 63 | end 64 | end 65 | return result 66 | end 67 | 68 | --maps a sequence inplace, modifying it {a, b, c} -> {f(a), f(b), f(c)} 69 | -- (automatically drops any nils, which can be used to simultaneously map and filter) 70 | function functional.map_inplace(t, f) 71 | local write_i = 0 72 | local n = #t --cache, so splitting the sequence doesn't stop iteration 73 | for i = 1, n do 74 | local v = f(t[i], i) 75 | if v ~= nil then 76 | write_i = write_i + 1 77 | t[write_i] = v 78 | end 79 | if i ~= write_i then 80 | t[i] = nil 81 | end 82 | end 83 | return t 84 | end 85 | 86 | --alias 87 | functional.remap = functional.map_inplace 88 | 89 | --maps a sequence {a, b, c} -> {a[k], b[k], c[k]} 90 | -- (automatically drops any nils to keep a sequence) 91 | function functional.map_field(t, k) 92 | local result = {} 93 | for i = 1, #t do 94 | local v = t[i][k] 95 | if v ~= nil then 96 | table.insert(result, v) 97 | end 98 | end 99 | return result 100 | end 101 | 102 | --maps a sequence by a method call 103 | -- if m is a string method name like "position", {a, b} -> {a:m(...), b:m(...)} 104 | -- if m is function reference like player.get_position, {a, b} -> {m(a, ...), m(b, ...)} 105 | -- (automatically drops any nils to keep a sequence) 106 | function functional.map_call(t, m, ...) 107 | local result = {} 108 | for i = 1, #t do 109 | local v = t[i] 110 | local f = type(m) == "function" and m or v[m] 111 | v = f(v, ...) 112 | if v ~= nil then 113 | table.insert(result, v) 114 | end 115 | end 116 | return result 117 | end 118 | 119 | --maps a sequence into a new index space (see functional.map) 120 | -- the function may return an index where the value will be stored in the result 121 | -- if no index (or a nil index) is provided, it will insert as normal 122 | function functional.splat(t, f) 123 | local result = {} 124 | for i = 1, #t do 125 | local v, pos = f(t[i], i) 126 | if v ~= nil then 127 | if pos == nil then 128 | pos = #result + 1 129 | end 130 | result[pos] = v 131 | end 132 | end 133 | return result 134 | end 135 | 136 | --filters a sequence 137 | -- returns a table containing items where f(v, i) returns truthy 138 | function functional.filter(t, f) 139 | local result = {} 140 | for i = 1, #t do 141 | local v = t[i] 142 | if f(v, i) then 143 | table.insert(result, v) 144 | end 145 | end 146 | return result 147 | end 148 | 149 | --filters a sequence in place, modifying it 150 | function functional.filter_inplace(t, f) 151 | local write_i = 0 152 | local n = #t --cache, so splitting the sequence doesn't stop iteration 153 | for i = 1, n do 154 | local v = t[i] 155 | if f(v, i) then 156 | write_i = write_i + 1 157 | t[write_i] = v 158 | end 159 | if i ~= write_i then 160 | t[i] = nil 161 | end 162 | end 163 | return t 164 | end 165 | 166 | -- complement of filter 167 | -- returns a table containing items where f(v) returns falsey 168 | -- nil results are included so that this is an exact complement of filter; consider using partition if you need both! 169 | function functional.remove_if(t, f) 170 | local result = {} 171 | for i = 1, #t do 172 | local v = t[i] 173 | if not f(v, i) then 174 | table.insert(result, v) 175 | end 176 | end 177 | return result 178 | end 179 | 180 | --partitions a sequence into two, based on filter criteria 181 | --simultaneous filter and remove_if 182 | function functional.partition(t, f) 183 | local a = {} 184 | local b = {} 185 | for i = 1, #t do 186 | local v = t[i] 187 | if f(v, i) then 188 | table.insert(a, v) 189 | else 190 | table.insert(b, v) 191 | end 192 | end 193 | return a, b 194 | end 195 | 196 | -- returns a table where the elements in t are grouped into sequential tables by the result of f on each element. 197 | -- more general than partition, but requires you to know your groups ahead of time 198 | -- (or use numeric grouping and pre-seed) if you want to avoid pairs! 199 | function functional.group_by(t, f) 200 | local result = {} 201 | for i = 1, #t do 202 | local v = t[i] 203 | local group = f(v, i) 204 | if result[group] == nil then 205 | result[group] = {} 206 | end 207 | table.insert(result[group], v) 208 | end 209 | return result 210 | end 211 | 212 | --combines two same-length sequences through a function f 213 | -- f receives arguments (t1[i], t2[i], i) 214 | -- iteration limited by min(#t1, #t2) 215 | -- ignores nil results 216 | function functional.combine(t1, t2, f) 217 | local ret = {} 218 | local limit = math.min(#t1, #t2) 219 | for i = 1, limit do 220 | local v1 = t1[i] 221 | local v2 = t2[i] 222 | local zipped = f(v1, v2, i) 223 | if zipped ~= nil then 224 | table.insert(ret, zipped) 225 | end 226 | end 227 | return ret 228 | end 229 | 230 | --zips two sequences together into a new table, alternating from t1 and t2 231 | -- zip({1, 2}, {3, 4}) -> {1, 3, 2, 4} 232 | -- iteration limited by min(#t1, #t2) 233 | function functional.zip(t1, t2) 234 | local ret = {} 235 | local limit = math.min(#t1, #t2) 236 | for i = 1, limit do 237 | table.insert(ret, t1[i]) 238 | table.insert(ret, t2[i]) 239 | end 240 | return ret 241 | end 242 | 243 | --unzips a table into two new tables, alternating elements into each result 244 | -- {1, 2, 3, 4} -> {1, 3}, {2, 4} 245 | -- gets an extra result in the first result for odd-length tables 246 | function functional.unzip(t) 247 | local a = {} 248 | local b = {} 249 | for i, v in ipairs(t) do 250 | table.insert(i % 2 == 1 and a or b, v) 251 | end 252 | return a, b 253 | end 254 | 255 | ----------------------------------------------------------- 256 | --specialised maps 257 | -- (experimental: let me know if you have better names for these!) 258 | ----------------------------------------------------------- 259 | 260 | --maps a sequence {a, b, c} -> collapse { f(a), f(b), f(c) } 261 | -- (ie results from functions should generally be sequences, 262 | -- which are appended onto each other, resulting in one big sequence) 263 | -- (automatically drops any nils, same as map) 264 | function functional.stitch(t, f) 265 | local result = {} 266 | for i, v in ipairs(t) do 267 | v = f(v, i) 268 | if v ~= nil then 269 | if type(v) == "table" then 270 | for _, e in ipairs(v) do 271 | table.insert(result, e) 272 | end 273 | else 274 | table.insert(result, v) 275 | end 276 | end 277 | end 278 | return result 279 | end 280 | 281 | --alias 282 | functional.map_stitch = functional.stitch 283 | 284 | --maps a sequence {a, b, c} -> { f(a, b), f(b, c), f(c, a) } 285 | -- useful for inter-dependent data 286 | -- (automatically drops any nils, same as map) 287 | 288 | function functional.cycle(t, f) 289 | local result = {} 290 | for i, a in ipairs(t) do 291 | local b = t[mathx.wrap(i + 1, 1, #t + 1)] 292 | local v = f(a, b) 293 | if v ~= nil then 294 | table.insert(result, v) 295 | end 296 | end 297 | return result 298 | end 299 | 300 | functional.map_cycle = functional.cycle 301 | 302 | --maps a sequence {a, b, c} -> { f(a, b), f(b, c) } 303 | -- useful for inter-dependent data 304 | -- (automatically drops any nils, same as map) 305 | 306 | function functional.chain(t, f) 307 | local result = {} 308 | for i = 2, #t do 309 | local a = t[i-1] 310 | local b = t[i] 311 | local v = f(a, b) 312 | if v ~= nil then 313 | table.insert(result, v) 314 | end 315 | end 316 | return result 317 | end 318 | 319 | functional.map_chain = functional.chain 320 | 321 | --maps a sequence {a, b, c, d} -> { f(a, b), f(a, c), f(a, d), f(b, c), f(b, d), f(c, d) } 322 | -- ie all distinct pairs are mapped, useful for any N^2 dataset (eg finding neighbours) 323 | 324 | function functional.map_pairs(t, f) 325 | local result = {} 326 | for i = 1, #t do 327 | for j = i+1, #t do 328 | local a = t[i] 329 | local b = t[j] 330 | local v = f(a, b) 331 | if v ~= nil then 332 | table.insert(result, v) 333 | end 334 | end 335 | end 336 | return result 337 | end 338 | 339 | 340 | ----------------------------------------------------------- 341 | --generating data 342 | ----------------------------------------------------------- 343 | 344 | --generate data into a table 345 | --basically a map on numeric values from 1 to count 346 | --nil values are omitted in the result, as for map 347 | function functional.generate(count, f) 348 | local result = {} 349 | for i = 1, count do 350 | local v = f(i) 351 | if v ~= nil then 352 | table.insert(result, v) 353 | end 354 | end 355 | return result 356 | end 357 | 358 | --2d version of the above 359 | --note: ends up with a 1d table; 360 | -- if you need a 2d table, you should nest 1d generate calls 361 | function functional.generate_2d(width, height, f) 362 | local result = {} 363 | for y = 1, height do 364 | for x = 1, width do 365 | local v = f(x, y) 366 | if v ~= nil then 367 | table.insert(result, v) 368 | end 369 | end 370 | end 371 | return result 372 | end 373 | 374 | ----------------------------------------------------------- 375 | --common queries and reductions 376 | ----------------------------------------------------------- 377 | 378 | --true if any element of the table matches f 379 | function functional.any(t, f) 380 | for i = 1, #t do 381 | if f(t[i], i) then 382 | return true 383 | end 384 | end 385 | return false 386 | end 387 | 388 | --true if no element of the table matches f 389 | function functional.none(t, f) 390 | for i = 1, #t do 391 | if f(t[i], i) then 392 | return false 393 | end 394 | end 395 | return true 396 | end 397 | 398 | --true if all elements of the table match f 399 | function functional.all(t, f) 400 | for i = 1, #t do 401 | if not f(t[i], i) then 402 | return false 403 | end 404 | end 405 | return true 406 | end 407 | 408 | --counts the elements of t that match f 409 | function functional.count(t, f) 410 | local c = 0 411 | for i = 1, #t do 412 | if f(t[i], i) then 413 | c = c + 1 414 | end 415 | end 416 | return c 417 | end 418 | 419 | --counts the elements of t equal to v 420 | function functional.count_value(t, v) 421 | local c = 0 422 | for i = 1, #t do 423 | if t[i] == v then 424 | c = c + 1 425 | end 426 | end 427 | return c 428 | end 429 | 430 | --true if the table contains element e 431 | function functional.contains(t, e) 432 | for i = 1, #t do 433 | if t[i] == e then 434 | return true 435 | end 436 | end 437 | return false 438 | end 439 | 440 | --return the numeric sum of all elements of t 441 | function functional.sum(t) 442 | local c = 0 443 | for i = 1, #t do 444 | c = c + t[i] 445 | end 446 | return c 447 | end 448 | 449 | --return the numeric mean of all elements of t 450 | function functional.mean(t) 451 | local len = #t 452 | if len == 0 then 453 | return 0 454 | end 455 | return functional.sum(t) / len 456 | end 457 | 458 | --return the minimum and maximum of t in one pass 459 | --or zero for both if t is empty 460 | -- (would perhaps more correctly be math.huge, -math.huge 461 | -- but that tends to be surprising/annoying in practice) 462 | function functional.minmax(t) 463 | local n = #t 464 | if n == 0 then 465 | return 0, 0 466 | end 467 | local max = t[1] 468 | local min = t[1] 469 | for i = 2, n do 470 | local v = t[i] 471 | min = math.min(min, v) 472 | max = math.max(max, v) 473 | end 474 | return min, max 475 | end 476 | 477 | --return the maximum element of t or zero if t is empty 478 | function functional.max(t) 479 | local min, max = functional.minmax(t) 480 | return max 481 | end 482 | 483 | --return the minimum element of t or zero if t is empty 484 | function functional.min(t) 485 | local min, max = functional.minmax(t) 486 | return min 487 | end 488 | 489 | --return the element of the table that results in the lowest numeric value 490 | --(function receives element and index respectively) 491 | function functional.find_min(t, f) 492 | local current = nil 493 | local current_min = math.huge 494 | for i = 1, #t do 495 | local e = t[i] 496 | local v = f(e, i) 497 | if v and v < current_min then 498 | current_min = v 499 | current = e 500 | end 501 | end 502 | return current 503 | end 504 | 505 | --return the element of the table that results in the greatest numeric value 506 | --(function receives element and index respectively) 507 | function functional.find_max(t, f) 508 | local current = nil 509 | local current_max = -math.huge 510 | for i = 1, #t do 511 | local e = t[i] 512 | local v = f(e, i) 513 | if v and v > current_max then 514 | current_max = v 515 | current = e 516 | end 517 | end 518 | return current 519 | end 520 | 521 | --alias 522 | functional.find_best = functional.find_max 523 | 524 | --return the element of the table that results in the value nearest to the passed value 525 | --todo: optimise, inline as this generates a closure each time 526 | function functional.find_nearest(t, f, target) 527 | local current = nil 528 | local current_min = math.huge 529 | for i = 1, #t do 530 | local e = t[i] 531 | local v = math.abs(f(e, i) - target) 532 | if v and v < current_min then 533 | current_min = v 534 | current = e 535 | if v == 0 then 536 | break 537 | end 538 | end 539 | end 540 | return current 541 | end 542 | 543 | --return the first element of the table that results in a true filter 544 | function functional.find_match(t, f) 545 | for i = 1, #t do 546 | local v = t[i] 547 | if f(v) then 548 | return v 549 | end 550 | end 551 | return nil 552 | end 553 | 554 | return functional 555 | -------------------------------------------------------------------------------- /identifier.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | identifier generation 3 | 4 | uuid is version 4, ulid is an alternative to uuid (see 5 | https://github.com/ulid/spec). 6 | 7 | todo: 8 | this ulid isn't guaranteed to be sortable for ulids generated 9 | within the same second yet 10 | ]] 11 | 12 | local path = (...):gsub("identifier", "") 13 | 14 | local identifier = {} 15 | 16 | --(internal; use a provided random generator object, or not) 17 | local function _random(rng, ...) 18 | if rng then return rng:random(...) end 19 | if love then return love.math.random(...) end 20 | return math.random(...) 21 | end 22 | 23 | local uuid4_template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" 24 | 25 | --generate a UUID version 4 26 | function identifier.uuid4(rng) 27 | --x should be 0x0-0xf, the single y should be 0x8-0xb 28 | --4 should always just be 4 (denoting uuid version) 29 | local out = uuid4_template:gsub("[xy]", function (c) 30 | return string.format( 31 | "%x", 32 | c == "x" and _random(rng, 0x0, 0xf) or _random(rng, 0x8, 0xb) 33 | ) 34 | end) 35 | 36 | return out 37 | end 38 | 39 | --crockford's base32 https://en.wikipedia.org/wiki/Base32 40 | local _encoding = { 41 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", 42 | "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "M", 43 | "N", "P", "Q", "R", "S", "T", "V", "W", "X", "Y", "Z" 44 | } 45 | 46 | --since ulid needs time since unix epoch with miliseconds, we can just 47 | --use socket. if that's not loaded, they'll have to provide their own 48 | local function _now(time_func, ...) 49 | if package.loaded.socket then return package.loaded.socket.gettime(...) end 50 | if pcall(require, "socket") then return require("socket").gettime(...) end 51 | if time_func then return time_func(...) end 52 | error("assertion failed: socket can't be found and no time function provided") 53 | end 54 | 55 | --generate an ULID using this rng at this time (now by default) 56 | --implementation based on https://github.com/Tieske/ulid.lua 57 | function identifier.ulid(rng, time) 58 | time = math.floor((time or _now()) * 1000) 59 | 60 | local time_part = {} 61 | local random_part = {} 62 | 63 | for i = 10, 1, -1 do 64 | local mod = time % #_encoding 65 | time_part[i] = _encoding[mod + 1] 66 | time = (time - mod) / #_encoding 67 | end 68 | 69 | for i = 1, 16 do 70 | random_part[i] = _encoding[math.floor(_random(rng) * #_encoding) + 1] 71 | end 72 | 73 | return table.concat(time_part) .. table.concat(random_part) 74 | end 75 | 76 | return identifier 77 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | batteries for lua 3 | 4 | a collection of helpful code to get your project off the ground faster 5 | ]] 6 | 7 | local path = ... 8 | local function require_relative(p) 9 | return require(table.concat({path, p}, ".")) 10 | end 11 | 12 | --build the module 13 | local _batteries = { 14 | -- 15 | class = require_relative("class"), 16 | -- 17 | assert = require_relative("assert"), 18 | --extension libraries 19 | mathx = require_relative("mathx"), 20 | tablex = require_relative("tablex"), 21 | stringx = require_relative("stringx"), 22 | --sorting routines 23 | sort = require_relative("sort"), 24 | -- 25 | functional = require_relative("functional"), 26 | --collections 27 | sequence = require_relative("sequence"), 28 | set = require_relative("set"), 29 | --geom 30 | vec2 = require_relative("vec2"), 31 | vec3 = require_relative("vec3"), 32 | intersect = require_relative("intersect"), 33 | -- 34 | timer = require_relative("timer"), 35 | pubsub = require_relative("pubsub"), 36 | state_machine = require_relative("state_machine"), 37 | async = require_relative("async"), 38 | manual_gc = require_relative("manual_gc"), 39 | colour = require_relative("colour"), 40 | pretty = require_relative("pretty"), 41 | measure = require_relative("measure"), 42 | make_pooled = require_relative("make_pooled"), 43 | pathfind = require_relative("pathfind"), 44 | } 45 | 46 | --assign aliases 47 | for _, alias in ipairs({ 48 | {"mathx", "math"}, 49 | {"tablex", "table"}, 50 | {"stringx", "string"}, 51 | {"sort", "stable_sort"}, 52 | {"colour", "color"}, 53 | }) do 54 | _batteries[alias[2]] = _batteries[alias[1]] 55 | end 56 | 57 | --easy export globally if required 58 | function _batteries:export() 59 | --export all key strings globally, if doesn't already exist 60 | for k, v in pairs(self) do 61 | if _G[k] == nil then 62 | _G[k] = v 63 | end 64 | end 65 | 66 | --overlay tablex and functional and sort routines onto table 67 | self.tablex.shallow_overlay(table, self.tablex) 68 | --now we can use it through table directly 69 | table.shallow_overlay(table, self.functional) 70 | self.sort:export() 71 | 72 | --overlay onto global math table 73 | table.shallow_overlay(math, self.mathx) 74 | 75 | --overlay onto string 76 | table.shallow_overlay(string, self.stringx) 77 | 78 | --overwrite assert wholesale (it's compatible) 79 | assert = self.assert 80 | 81 | --like ipairs, but in reverse 82 | ripairs = self.tablex.ripairs 83 | 84 | --export the whole library to global `batteries` 85 | batteries = self 86 | 87 | return self 88 | end 89 | 90 | 91 | --convert naming, for picky eaters 92 | --experimental, let me know how it goes 93 | function _batteries:camelCase() 94 | --not part of stringx for now, because it's not necessarily utf8 safe 95 | local function capitalise(s) 96 | local head = s:sub(1,1) 97 | local tail = s:sub(2) 98 | return head:upper() .. tail 99 | end 100 | 101 | --any acronyms to fully capitalise to avoid "Rgb" and the like 102 | local acronyms = _batteries.set{"rgb", "rgba", "argb", "hsl", "xy", "gc", "aabb",} 103 | local function caps_acronym(s) 104 | if acronyms:has(s) then 105 | s = s:upper() 106 | end 107 | return s 108 | end 109 | 110 | --convert something_like_this to somethingLikeThis 111 | local function snake_to_camel(s) 112 | local chunks = _batteries.sequence(_batteries.stringx.split(s, "_")) 113 | chunks:remap(caps_acronym) 114 | local first = chunks:shift() 115 | chunks:remap(capitalise) 116 | chunks:unshift(first) 117 | return chunks:concat("") 118 | end 119 | --convert all named properties 120 | --(keep the old ones around as well) 121 | --(we take a copy of the keys here cause we're going to be inserting new keys as we go) 122 | for _, k in ipairs(_batteries.tablex.keys(self)) do 123 | local v = self[k] 124 | if 125 | --only convert string properties 126 | type(k) == "string" 127 | --ignore private and metamethod properties 128 | and not _batteries.stringx.starts_with(k, "_") 129 | then 130 | --convert 131 | local camel = snake_to_camel(k) 132 | if type(v) == "table" then 133 | --capitalise classes 134 | if v.__index == v then 135 | camel = capitalise(camel) 136 | --modify the internal name for :type() 137 | --might be a problem for serialisation etc, 138 | --but i imagine converting to/from camelCase mid-project is rare 139 | v.__name = camel 140 | end 141 | --recursively convert anything nested as well 142 | _batteries.camelCase(v) 143 | end 144 | --assign if the key changed and there isn't a matching key 145 | if k ~= camel and self[camel] == nil then 146 | self[camel] = v 147 | end 148 | end 149 | end 150 | 151 | return self 152 | end 153 | 154 | return _batteries 155 | -------------------------------------------------------------------------------- /intersect.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | geometric intersection routines 3 | 4 | from simple point tests to shape vs shape tests 5 | 6 | optimised pretty well in most places 7 | 8 | tests provided: 9 | overlap 10 | boolean "is overlapping" 11 | collide 12 | nil for no collision 13 | minimum separating vector on collision 14 | provided in the direction of the first object 15 | optional output parameters to avoid garbage generation 16 | ]] 17 | 18 | local path = (...):gsub("intersect", "") 19 | local vec2 = require(path .. "vec2") 20 | local mathx = require(path .. "mathx") 21 | 22 | --module storage 23 | local intersect = {} 24 | 25 | --epsilon for collisions 26 | local COLLIDE_EPS = 1e-6 27 | 28 | ------------------------------------------------------------------------------ 29 | -- circles 30 | 31 | function intersect.circle_point_overlap(pos, rad, v) 32 | return pos:distance_squared(v) <= rad * rad 33 | end 34 | 35 | function intersect.circle_circle_overlap(a_pos, a_rad, b_pos, b_rad) 36 | local rad = a_rad + b_rad 37 | return a_pos:distance_squared(b_pos) <= rad * rad 38 | end 39 | 40 | function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into) 41 | --get delta 42 | local delta = a_pos 43 | :pooled_copy() 44 | :vector_sub_inplace(b_pos) 45 | --squared threshold 46 | local rad = a_rad + b_rad 47 | local dist = delta:length_squared() 48 | local res = false 49 | if dist <= rad * rad then 50 | if dist == 0 then 51 | --singular case; just resolve vertically 52 | dist = 1 53 | delta:scalar_set(0, 1) 54 | else 55 | --get actual distance 56 | dist = math.sqrt(dist) 57 | end 58 | --allocate if needed 59 | if into == nil then 60 | into = vec2(0) 61 | end 62 | --normalise, scale to separating distance 63 | res = into:set(delta) 64 | :scalar_div_inplace(dist) 65 | :scalar_mul_inplace(rad - dist) 66 | end 67 | delta:release() 68 | return res 69 | end 70 | 71 | function intersect.circle_point_collide(a_pos, a_rad, b, into) 72 | return intersect.circle_circle_collide(a_pos, a_rad, b, 0, into) 73 | end 74 | 75 | ------------------------------------------------------------------------------ 76 | -- line segments 77 | -- todo: separate double-sided, one-sided, and pull-through (along normal) collisions? 78 | 79 | --get the nearest point on the line segment a from point b 80 | function intersect.nearest_point_on_line(a_start, a_end, b_pos, into) 81 | if into == nil then into = vec2(0) end 82 | --direction of segment 83 | local segment = a_end:pooled_copy() 84 | :vector_sub_inplace(a_start) 85 | --detect degenerate case 86 | local lensq = segment:length_squared() 87 | if lensq <= COLLIDE_EPS then 88 | into:set(a_start) 89 | else 90 | --solve for factor along segment 91 | local point_to_start = b_pos:pooled_copy() 92 | :vector_sub_inplace(a_start) 93 | local factor = mathx.clamp01(point_to_start:dot(segment) / lensq) 94 | into:set(segment) 95 | :scalar_mul_inplace(factor) 96 | :vector_add_inplace(a_start) 97 | point_to_start:release() 98 | end 99 | segment:release() 100 | return into 101 | end 102 | 103 | --internal 104 | --vector from line seg origin to point 105 | function intersect._line_to_point(a_start, a_end, b_pos, into) 106 | return intersect.nearest_point_on_line(a_start, a_end, b_pos, into) 107 | :vector_sub_inplace(b_pos) 108 | end 109 | 110 | --internal 111 | --line displacement vector from separation vector 112 | function intersect._line_displacement_to_sep(a_start, a_end, separation, total_rad) 113 | local distance = separation:normalise_len_inplace() 114 | local sep = distance - total_rad 115 | if sep <= 0 then 116 | if distance <= COLLIDE_EPS then 117 | --point intersecting the line; push out along normal 118 | separation:set(a_end) 119 | :vector_sub_inplace(a_start) 120 | :normalise_inplace() 121 | :rot90l_inplace() 122 | else 123 | separation:scalar_mul_inplace(-sep) 124 | end 125 | return separation 126 | end 127 | return false 128 | end 129 | 130 | --overlap a line segment with a circle 131 | function intersect.line_circle_overlap(a_start, a_end, a_rad, b_pos, b_rad) 132 | local nearest = intersect.nearest_point_on_line(a_start, a_end, b_pos, vec2:pooled()) 133 | local overlapped = intersect.circle_point_overlap(b_pos, a_rad + b_rad, nearest) 134 | nearest:release() 135 | return overlapped 136 | end 137 | 138 | --collide a line segment with a circle 139 | function intersect.line_circle_collide(a_start, a_end, a_rad, b_pos, b_rad, into) 140 | local nearest = intersect.nearest_point_on_line(a_start, a_end, b_pos, vec2:pooled()) 141 | into = intersect.circle_circle_collide(nearest, a_rad, b_pos, b_rad, into) 142 | nearest:release() 143 | return into 144 | end 145 | 146 | --collide 2 line segments 147 | local _line_line_search_tab = { 148 | {vec2(), 1}, 149 | {vec2(), 1}, 150 | {vec2(), -1}, 151 | {vec2(), -1}, 152 | } 153 | function intersect.line_line_collide(a_start, a_end, a_rad, b_start, b_end, b_rad, into) 154 | --segment directions from start points 155 | local a_dir = a_end 156 | :pooled_copy() 157 | :vector_sub_inplace(a_start) 158 | local b_dir = b_end 159 | :pooled_copy() 160 | :vector_sub_inplace(b_start) 161 | 162 | --detect degenerate cases 163 | local a_degen = a_dir:length_squared() <= COLLIDE_EPS 164 | local b_degen = b_dir:length_squared() <= COLLIDE_EPS 165 | if a_degen or b_degen then 166 | vec2.release(a_dir, b_dir) 167 | if a_degen and b_degen then 168 | --actually just circles 169 | return intersect.circle_circle_collide(a_start, a_rad, b_start, b_rad, into) 170 | elseif a_degen then 171 | --a is just circle 172 | return intersect.circle_line_collide(a_start, a_rad, b_start, b_end, b_rad, into) 173 | elseif b_degen then 174 | --b is just circle 175 | return intersect.line_circle_collide(a_start, a_end, a_rad, b_start, b_rad, into) 176 | else 177 | error("should be unreachable") 178 | end 179 | end 180 | --otherwise we're _actually_ 2 line segs :) 181 | if into == nil then into = vec2(0) end 182 | 183 | --first, check intersection 184 | 185 | --(c to lua translation of paul bourke's 186 | -- line intersection algorithm) 187 | local dx1 = (a_end.x - a_start.x) 188 | local dx2 = (b_end.x - b_start.x) 189 | local dy1 = (a_end.y - a_start.y) 190 | local dy2 = (b_end.y - b_start.y) 191 | local dxab = (a_start.x - b_start.x) 192 | local dyab = (a_start.y - b_start.y) 193 | 194 | local denom = dy2 * dx1 - dx2 * dy1 195 | local numera = dx2 * dyab - dy2 * dxab 196 | local numerb = dx1 * dyab - dy1 * dxab 197 | 198 | --check coincident lines 199 | local intersected 200 | if 201 | math.abs(numera) == 0 and 202 | math.abs(numerb) == 0 and 203 | math.abs(denom) == 0 204 | then 205 | intersected = "both" 206 | else 207 | --check parallel, non-coincident lines 208 | if math.abs(denom) == 0 then 209 | intersected = "none" 210 | else 211 | --get interpolants along segments 212 | local mua = numera / denom 213 | local mub = numerb / denom 214 | --intersection outside segment bounds? 215 | local outside_a = mua < 0 or mua > 1 216 | local outside_b = mub < 0 or mub > 1 217 | if outside_a and outside_b then 218 | intersected = "none" 219 | elseif outside_a then 220 | intersected = "b" 221 | elseif outside_b then 222 | intersected = "a" 223 | else 224 | intersected = "both" 225 | end 226 | end 227 | end 228 | assert(intersected) 229 | 230 | if intersected == "both" then 231 | --simply displace along A normal 232 | into:set(a_dir) 233 | vec2.release(a_dir, b_dir) 234 | return into 235 | :normalise_inplace() 236 | :scalar_mul_inplace(a_rad + b_rad) 237 | :rot90l_inplace() 238 | end 239 | 240 | vec2.release(a_dir, b_dir) 241 | 242 | --dumb as a rocks check-corners approach 243 | --todo proper calculus from http://geomalgorithms.com/a07-_distance.html 244 | local search_tab = _line_line_search_tab 245 | for i = 1, 4 do 246 | search_tab[i][1]:sset(math.huge) 247 | end 248 | --only insert corners from the non-intersected line 249 | --since intersected line is potentially the apex 250 | if intersected ~= "a" then 251 | --a endpoints 252 | intersect._line_to_point(b_start, b_end, a_start, search_tab[1][1]) 253 | intersect._line_to_point(b_start, b_end, a_end, search_tab[2][1]) 254 | end 255 | if intersected ~= "b" then 256 | --b endpoints 257 | intersect._line_to_point(a_start, a_end, b_start, search_tab[3][1]) 258 | intersect._line_to_point(a_start, a_end, b_end, search_tab[4][1]) 259 | end 260 | 261 | local best = nil 262 | local best_len = nil 263 | for _, v in ipairs(search_tab) do 264 | local delta = v[1] 265 | if delta.x ~= math.huge then 266 | local len = delta:length_squared() 267 | if len < (best_len or math.huge) then 268 | best = v 269 | end 270 | end 271 | end 272 | 273 | --fix direction 274 | into:set(best[1]) 275 | :scalar_mul_inplace(best[2]) 276 | 277 | return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad) 278 | end 279 | 280 | ------------------------------------------------------------------------------ 281 | -- axis aligned bounding boxes 282 | -- 283 | -- pos is the centre position of the box 284 | -- hs is the half-size of the box 285 | -- eg for a 10x8 box, vec2(5, 4) 286 | -- 287 | -- we use half-sizes to keep these routines as fast as possible 288 | -- see intersect.rect_to_aabb for conversion from topleft corner and size 289 | 290 | --return true on overlap, false otherwise 291 | function intersect.aabb_point_overlap(pos, hs, v) 292 | local delta = pos 293 | :pooled_copy() 294 | :vector_sub_inplace(v) 295 | :abs_inplace() 296 | local overlap = delta.x <= hs.x and delta.y <= hs.y 297 | delta:release() 298 | return overlap 299 | end 300 | 301 | -- discrete displacement 302 | -- return msv to push point to closest edge of aabb 303 | function intersect.aabb_point_collide(pos, hs, v, into) 304 | --separation between centres 305 | local delta_c = v 306 | :pooled_copy() 307 | :vector_sub_inplace(pos) 308 | --absolute separation 309 | local delta_c_abs = delta_c 310 | :pooled_copy() 311 | :abs_inplace() 312 | local res = false 313 | if delta_c_abs.x < hs.x and delta_c_abs.y < hs.y then 314 | res = (into or vec2(0)) 315 | --separating offset in both directions 316 | :set(hs) 317 | :vector_sub_inplace(delta_c_abs) 318 | --minimum separating distance 319 | :minor_inplace() 320 | --in the right direction 321 | :vector_mul_inplace(delta_c:sign_inplace()) 322 | --from the aabb's point of view 323 | :inverse_inplace() 324 | end 325 | vec2.release(delta_c, delta_c_abs) 326 | return res 327 | end 328 | 329 | --return true on overlap, false otherwise 330 | function intersect.aabb_aabb_overlap(a_pos, a_hs, b_pos, b_hs) 331 | local delta = a_pos 332 | :pooled_copy() 333 | :vector_sub_inplace(b_pos) 334 | :abs_inplace() 335 | local total_size = a_hs 336 | :pooled_copy() 337 | :vector_add_inplace(b_hs) 338 | local overlap = delta.x <= total_size.x and delta.y <= total_size.y 339 | vec2.release(delta, total_size) 340 | return overlap 341 | end 342 | 343 | --discrete displacement 344 | --return msv on collision, false otherwise 345 | function intersect.aabb_aabb_collide(a_pos, a_hs, b_pos, b_hs, into) 346 | local delta = a_pos 347 | :pooled_copy() 348 | :vector_sub_inplace(b_pos) 349 | local abs_delta = delta 350 | :pooled_copy() 351 | :abs_inplace() 352 | local size = a_hs 353 | :pooled_copy() 354 | :vector_add_inplace(b_hs) 355 | local abs_amount = size 356 | :pooled_copy() 357 | :vector_sub_inplace(abs_delta) 358 | local res = false 359 | if abs_amount.x > COLLIDE_EPS and abs_amount.y > COLLIDE_EPS then 360 | if not into then into = vec2(0) end 361 | --actually collided 362 | if abs_amount.x <= abs_amount.y then 363 | --x min 364 | res = into:scalar_set(abs_amount.x * mathx.sign(delta.x), 0) 365 | else 366 | --y min 367 | res = into:scalar_set(0, abs_amount.y * mathx.sign(delta.y)) 368 | end 369 | end 370 | return res 371 | end 372 | 373 | -- helper function to clamp point to aabb 374 | function intersect.aabb_point_clamp(pos, hs, v, into) 375 | local v_min = pos 376 | :pooled_copy() 377 | :vector_sub_inplace(hs) 378 | local v_max = pos 379 | :pooled_copy() 380 | :vector_add_inplace(hs) 381 | into = into or vec2(0) 382 | into:set(v) 383 | :clamp_inplace(v_min, v_max) 384 | vec2.release(v_min, v_max) 385 | return into 386 | end 387 | 388 | -- return true on overlap, false otherwise 389 | function intersect.aabb_circle_overlap(a_pos, a_hs, b_pos, b_rad) 390 | local clamped = intersect.aabb_point_clamp(a_pos, a_hs, b_pos, vec2:pooled()) 391 | local edge_distance_squared = clamped:distance_squared(b_pos) 392 | clamped:release() 393 | return edge_distance_squared <= (b_rad * b_rad) 394 | end 395 | 396 | -- return msv on collision, false otherwise 397 | function intersect.aabb_circle_collide(a_pos, a_hs, b_pos, b_rad, into) 398 | local abs_delta = a_pos 399 | :pooled_copy() 400 | :vector_sub_inplace(b_pos) 401 | :abs_inplace() 402 | --circle centre within aabb-like bounds, collide as an aabb 403 | local like_aabb = abs_delta.x < a_hs.x or abs_delta.y < a_hs.y 404 | --(clean up) 405 | abs_delta:release() 406 | -- 407 | local result 408 | if like_aabb then 409 | local pretend_hs = vec2:pooled(0, 0) 410 | result = intersect.aabb_aabb_collide(a_pos, a_hs, b_pos, pretend_hs, into) 411 | pretend_hs:release() 412 | else 413 | --outside aabb-like bounds so we need to collide with the nearest clamped corner point 414 | local clamped = intersect.aabb_point_clamp(a_pos, a_hs, b_pos, vec2:pooled()) 415 | result = intersect.circle_circle_collide(clamped, 0, b_pos, b_rad, into) 416 | clamped:release() 417 | end 418 | return result 419 | end 420 | 421 | --convert raw x, y, w, h rectangle components to aabb vectors 422 | function intersect.rect_raw_to_aabb(x, y, w, h) 423 | local hs = vec2(w, h):scalar_mul_inplace(0.5) 424 | local pos = vec2(x, y):vector_add_inplace(hs) 425 | return pos, hs 426 | end 427 | 428 | --convert (x, y), (w, h) rectangle vectors to aabb vectors 429 | function intersect.rect_to_aabb(pos, size) 430 | return intersect.rect_raw_to_aabb(pos.x, pos.y, size.x, size.y) 431 | end 432 | 433 | --check if a point is in a polygon 434 | --point is the point to test 435 | --poly is a list of points in order 436 | --based on winding number, so re-intersecting areas are counted as solid rather than inverting 437 | function intersect.point_in_poly(point, poly) 438 | local wn = 0 439 | for i, a in ipairs(poly) do 440 | local b = poly[i + 1] or poly[1] 441 | if a.y <= point.y then 442 | if 443 | b.y > point.y 444 | and vec2.winding_side(a, b, point) > 0 445 | then 446 | wn = wn + 1 447 | end 448 | else 449 | if 450 | b.y <= point.y 451 | and vec2.winding_side(a, b, point) < 0 452 | then 453 | wn = wn - 1 454 | end 455 | end 456 | end 457 | return wn ~= 0 458 | end 459 | 460 | --reversed versions 461 | --it's annoying to need to flip the order of operands depending on what 462 | --shapes you're working with 463 | --so these functions provide the 464 | 465 | --todo: ensure this is all of them 466 | 467 | --(helper for reversing only if there's actually a vector, preserving false) 468 | function intersect.reverse_msv(result) 469 | if result then 470 | result:inverse_inplace() 471 | end 472 | return result 473 | end 474 | 475 | function intersect.point_circle_overlap(a, b_pos, b_rad) 476 | return intersect.circle_point_overlap(b_pos, b_rad, a) 477 | end 478 | 479 | function intersect.point_circle_collide(a, b_pos, b_rad, into) 480 | return intersect.reverse_msv(intersect.circle_circle_collide(b_pos, b_rad, a, 0, into)) 481 | end 482 | 483 | function intersect.point_aabb_overlap(a, b_pos, b_hs) 484 | return intersect.aabb_point_overlap(b_pos, b_hs, a) 485 | end 486 | 487 | function intersect.point_aabb_collide(a, b_pos, b_hs, into) 488 | return intersect.reverse_msv(intersect.aabb_point_collide(b_pos, b_hs, a, into)) 489 | end 490 | 491 | function intersect.circle_aabb_overlap(a, a_rad, b_pos, b_hs) 492 | return intersect.aabb_circle_overlap(b_pos, b_hs, a, a_rad) 493 | end 494 | 495 | function intersect.circle_aabb_collide(a, a_rad, b_pos, b_hs, into) 496 | return intersect.reverse_msv(intersect.aabb_circle_collide(b_pos, b_hs, a, a_rad, into)) 497 | end 498 | 499 | function intersect.circle_line_collide(a, a_rad, b_start, b_end, b_rad, into) 500 | return intersect.reverse_msv(intersect.line_circle_collide(b_start, b_end, b_rad, a, a_rad, into)) 501 | end 502 | 503 | --resolution helpers 504 | 505 | --resolve a collision between two bodies, given a (minimum) separating vector 506 | -- from a's frame of reference, like the result of any of the _collide functions 507 | --requires the two positions of the bodies, the msv, and a balance factor 508 | --balance should be between 1 and 0; 509 | -- 1 is only a_pos moving to resolve 510 | -- 0 is only b_pos moving to resolve 511 | -- 0.5 is balanced between both (default) 512 | --note: this wont work as-is for line segments, which have two separate position coordinates 513 | -- you will need to understand what is going on and move the both coordinates yourself 514 | function intersect.resolve_msv(a_pos, b_pos, msv, balance) 515 | balance = balance or 0.5 516 | a_pos:fused_multiply_add_inplace(msv, balance) 517 | b_pos:fused_multiply_add_inplace(msv, -(1 - balance)) 518 | end 519 | 520 | -- gets a normalised balance factor from two mass inputs, and treats <=0 or infinite or nil masses as static bodies 521 | -- returns false if we're colliding two static bodies, as that's invalid 522 | function intersect.balance_from_mass(a_mass, b_mass) 523 | --static cases 524 | local a_static = not a_mass or a_mass <= 0 or a_mass == math.huge 525 | local b_static = not b_mass or b_mass <= 0 or b_mass == math.huge 526 | if a_static and b_static then 527 | return false --colliding two static bodies 528 | elseif a_static then 529 | return 0.0 530 | elseif b_static then 531 | return 1.0 532 | end 533 | 534 | --get balance factor 535 | local total = a_mass + b_mass 536 | return b_mass / total 537 | end 538 | 539 | --bounce a velocity off of a normal (modifying velocity) 540 | --essentially flips the part of the velocity in the direction of the normal 541 | function intersect.bounce_off(velocity, normal, conservation) 542 | --(default) 543 | conservation = conservation or 1 544 | --take a copy, we need it 545 | local old_vel = velocity:pooled_copy() 546 | --heading into the normal 547 | if old_vel:dot(normal) < 0 then 548 | --reject on the normal (keep velocity tangential to the normal) 549 | velocity:vector_rejection_inplace(normal) 550 | --add back the complement of the difference; 551 | --basically "flip" the velocity in line with the normal. 552 | velocity:fused_multiply_add_inplace(old_vel:vector_sub_inplace(velocity), -conservation) 553 | end 554 | --clean up 555 | old_vel:release() 556 | return velocity 557 | end 558 | 559 | --mutual bounce; two similar bodies bounce off each other, transferring energy 560 | function intersect.mutual_bounce(velocity_a, velocity_b, normal, conservation) 561 | --(default) 562 | conservation = conservation or 1 563 | --take copies, we need them 564 | local old_a_vel = velocity_a:pooled_copy() 565 | local old_b_vel = velocity_b:pooled_copy() 566 | --reject on the normal 567 | velocity_a:vector_rejection_inplace(normal) 568 | velocity_b:vector_rejection_inplace(normal) 569 | --calculate the amount remaining from the old velocity 570 | --(transfer pool ownership) 571 | local a_remaining = old_a_vel:vector_sub_inplace(velocity_a) 572 | local b_remaining = old_b_vel:vector_sub_inplace(velocity_b) 573 | --transfer it to the other body 574 | velocity_a:fused_multiply_add_inplace(b_remaining, conservation) 575 | velocity_b:fused_multiply_add_inplace(a_remaining, conservation) 576 | --clean up 577 | vec2.release(a_remaining, b_remaining) 578 | end 579 | 580 | return intersect 581 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 Max Cahill 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /make_pooled.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | add pooling functionality to a class 3 | 4 | adds a handful of class and instance methods to do with pooling 5 | 6 | todo: automatically use the pool by replacing __call, so you really just need to :release() 7 | ]] 8 | 9 | return function(class, limit) 10 | --shared pooled storage 11 | local _pool = {} 12 | --size limit for tuning memory upper bound 13 | local _pool_limit = limit or 128 14 | 15 | --flush the entire pool 16 | function class:flush_pool() 17 | if #_pool > 0 then 18 | _pool = {} 19 | end 20 | end 21 | 22 | --drain one element from the pool, if it exists 23 | function class:drain_pool() 24 | if #_pool > 0 then 25 | return table.remove(_pool) 26 | end 27 | return nil 28 | end 29 | 30 | --get a pooled object 31 | --(re-initialised with new, or freshly constructed if the pool was empty) 32 | function class:pooled(...) 33 | local instance = class:drain_pool() 34 | if not instance then 35 | return class(...) 36 | end 37 | instance:new(...) 38 | return instance 39 | end 40 | 41 | --release an object back to the pool 42 | function class.release(instance, ...) 43 | assert(instance:type() == class:type(), "wrong class released to pool") 44 | if #_pool < _pool_limit then 45 | table.insert(_pool, instance) 46 | end 47 | --recurse 48 | if ... then 49 | return class.release(...) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /manual_gc.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | "semi-manual" garbage collection 3 | 4 | specify a time budget and a memory ceiling per call. 5 | 6 | called once per frame, this will spread any big collections 7 | over several frames, and "catch up" when there is too much 8 | work to do. 9 | 10 | This keeps GC time burden much more predictable. 11 | 12 | The memory ceiling provides a safety backstop. 13 | if exceeded it will trigger a "full" collection, and this will 14 | hurt performance - you'll notice the hitch. If you hit your ceiling, 15 | it indicates you likely need to either find a way to generate less 16 | garbage, or spend more time each frame collecting. 17 | 18 | the function instructs the garbage collector only as small a step 19 | as possible each iteration. this prevents the "spiky" collection 20 | patterns, though with particularly large sets of tiny objects, 21 | the start of a collection can still take longer than you might like. 22 | 23 | default values: 24 | 25 | time_budget - 1ms (1e-3) 26 | adjust down or up as needed. games that generate more garbage 27 | will need to spend longer on gc each frame. 28 | 29 | memory_ceiling - unlimited 30 | a good place to start might be something like 64mb, though some games 31 | will need much more. remember, this is lua memory, not the total memory 32 | consumption of your game. 33 | 34 | disable_otherwise - false 35 | disabling the gc completely is dangerous - any big allocation 36 | event (eg - level gen) could push you to an out of memory 37 | situation and crash your game. test extensively before you 38 | ship a game with this set true. 39 | ]] 40 | 41 | return function(time_budget, memory_ceiling, disable_otherwise) 42 | time_budget = time_budget or 1e-3 43 | memory_ceiling = memory_ceiling or math.huge 44 | local max_steps = 1000 45 | local steps = 0 46 | local start_time = love.timer.getTime() 47 | while 48 | love.timer.getTime() - start_time < time_budget and 49 | steps < max_steps 50 | do 51 | if collectgarbage("step", 1) then 52 | break 53 | end 54 | steps = steps + 1 55 | end 56 | --safety net 57 | if collectgarbage("count") / 1024 > memory_ceiling then 58 | collectgarbage("collect") 59 | end 60 | --don't collect gc outside this margin 61 | if disable_otherwise then 62 | collectgarbage("stop") 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mathx.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | extra mathematical functions 3 | ]] 4 | 5 | local mathx = setmetatable({}, { 6 | __index = math, 7 | }) 8 | 9 | --wrap v around range [lo, hi) 10 | function mathx.wrap(v, lo, hi) 11 | return (v - lo) % (hi - lo) + lo 12 | end 13 | 14 | --wrap i around the indices of t 15 | function mathx.wrap_index(i, t) 16 | return math.floor(mathx.wrap(i, 1, #t + 1)) 17 | end 18 | 19 | --clamp v to range [lo, hi] 20 | function mathx.clamp(v, lo, hi) 21 | return math.max(lo, math.min(v, hi)) 22 | end 23 | 24 | --clamp v to range [0, 1] 25 | function mathx.clamp01(v) 26 | return mathx.clamp(v, 0, 1) 27 | end 28 | 29 | --round v to nearest whole, away from zero 30 | function mathx.round(v) 31 | if v < 0 then 32 | return math.ceil(v - 0.5) 33 | end 34 | return math.floor(v + 0.5) 35 | end 36 | 37 | --round v to one-in x 38 | -- (eg x = 2, v rounded to increments of 0.5) 39 | function mathx.to_one_in(v, x) 40 | return mathx.round(v * x) / x 41 | end 42 | 43 | --round v to a given decimal precision 44 | function mathx.to_precision(v, decimal_points) 45 | return mathx.to_one_in(v, math.pow(10, decimal_points)) 46 | end 47 | 48 | --0, 1, -1 sign of a scalar 49 | --todo: investigate if a branchless or `/abs` approach is faster in general case 50 | function mathx.sign(v) 51 | if v < 0 then return -1 end 52 | if v > 0 then return 1 end 53 | return 0 54 | end 55 | 56 | --linear interpolation between a and b 57 | function mathx.lerp(a, b, t) 58 | return a * (1.0 - t) + b * t 59 | end 60 | 61 | --linear interpolation with a minimum "final step" distance 62 | --useful for making sure dynamic lerps do actually reach their final destination 63 | function mathx.lerp_eps(a, b, t, eps) 64 | local v = mathx.lerp(a, b, t) 65 | if math.abs(v - b) < eps then 66 | v = b 67 | end 68 | return v 69 | end 70 | 71 | --bilinear interpolation between 4 samples 72 | function mathx.bilerp(a, b, c, d, u, v) 73 | return mathx.lerp( 74 | mathx.lerp(a, b, u), 75 | mathx.lerp(c, d, u), 76 | v 77 | ) 78 | end 79 | 80 | --get the lerp factor on a range, inverse_lerp(6, 0, 10) == 0.6 81 | function mathx.inverse_lerp(v, min, max) 82 | return (v - min) / (max - min) 83 | end 84 | 85 | --remap a value from one range to another 86 | function mathx.remap_range(v, in_min, in_max, out_min, out_max) 87 | return mathx.lerp(out_min, out_max, mathx.inverse_lerp(v, in_min, in_max)) 88 | end 89 | 90 | --remap a value from one range to another, staying within that range 91 | function mathx.remap_range_clamped(v, in_min, in_max, out_min, out_max) 92 | return mathx.lerp(out_min, out_max, mathx.clamp01(mathx.inverse_lerp(v, in_min, in_max))) 93 | end 94 | 95 | --easing curves 96 | --(generally only "safe" for 0-1 range, see mathx.clamp01) 97 | 98 | --no curve - can be used as a default to avoid needing a branch 99 | function mathx.identity(f) 100 | return f 101 | end 102 | 103 | --classic smoothstep 104 | function mathx.smoothstep(f) 105 | return f * f * (3 - 2 * f) 106 | end 107 | 108 | --classic smootherstep; zero 2nd order derivatives at 0 and 1 109 | function mathx.smootherstep(f) 110 | return f * f * f * (f * (f * 6 - 15) + 10) 111 | end 112 | 113 | --pingpong from 0 to 1 and back again 114 | function mathx.pingpong(f) 115 | return 1 - math.abs(1 - (f * 2) % 2) 116 | end 117 | 118 | --quadratic ease in 119 | function mathx.ease_in(f) 120 | return f * f 121 | end 122 | 123 | --quadratic ease out 124 | function mathx.ease_out(f) 125 | local oneminus = (1 - f) 126 | return 1 - oneminus * oneminus 127 | end 128 | 129 | --quadratic ease in and out 130 | --(a lot like smoothstep) 131 | function mathx.ease_inout(f) 132 | if f < 0.5 then 133 | return f * f * 2 134 | end 135 | local oneminus = (1 - f) 136 | return 1 - 2 * oneminus * oneminus 137 | end 138 | 139 | --branchless but imperfect quartic in/out 140 | --either smooth or smootherstep are usually a better alternative 141 | function mathx.ease_inout_branchless(f) 142 | local halfsquared = f * f / 2 143 | return halfsquared * (1 - halfsquared) * 4 144 | end 145 | 146 | --todo: more easings - back, bounce, elastic 147 | 148 | --(internal; use a provided random generator object, or not) 149 | local function _random(rng, ...) 150 | if rng then return rng:random(...) end 151 | if love then return love.math.random(...) end 152 | return math.random(...) 153 | end 154 | 155 | --return a random sign 156 | function mathx.random_sign(rng) 157 | return _random(rng) < 0.5 and -1 or 1 158 | end 159 | 160 | --return a random value between two numbers (continuous) 161 | function mathx.random_lerp(min, max, rng) 162 | return mathx.lerp(min, max, _random(rng)) 163 | end 164 | 165 | --nan checking 166 | function mathx.isnan(v) 167 | return v ~= v 168 | end 169 | 170 | --angle handling stuff 171 | --superior constant handy for expressing things in turns 172 | mathx.tau = math.pi * 2 173 | 174 | --normalise angle onto the interval [-math.pi, math.pi) 175 | --so each angle only has a single value representing it 176 | function mathx.normalise_angle(a) 177 | return mathx.wrap(a, -math.pi, math.pi) 178 | end 179 | 180 | --alias for americans 181 | mathx.normalize_angle = mathx.normalise_angle 182 | 183 | --get the normalised difference between two angles 184 | function mathx.angle_difference(a, b) 185 | a = mathx.normalise_angle(a) 186 | b = mathx.normalise_angle(b) 187 | return mathx.normalise_angle(b - a) 188 | end 189 | 190 | --mathx.lerp equivalent for angles 191 | function mathx.lerp_angle(a, b, t) 192 | local dif = mathx.angle_difference(a, b) 193 | return mathx.normalise_angle(a + dif * t) 194 | end 195 | 196 | --mathx.lerp_eps equivalent for angles 197 | function mathx.lerp_angle_eps(a, b, t, eps) 198 | --short circuit to avoid having to wrap so many angles 199 | if a == b then 200 | return a 201 | end 202 | --same logic as lerp_eps 203 | local v = mathx.lerp_angle(a, b, t) 204 | if math.abs(mathx.angle_difference(v, b)) < eps then 205 | v = b 206 | end 207 | return v 208 | end 209 | 210 | --geometric functions standalone/"unpacked" components and multi-return 211 | --consider using vec2 if you need anything complex! 212 | 213 | --rotate a point around the origin by an angle 214 | function mathx.rotate(x, y, r) 215 | local s = math.sin(r) 216 | local c = math.cos(r) 217 | return c * x - s * y, s * x + c * y 218 | end 219 | 220 | --get the length of a vector from the origin 221 | function mathx.length(x, y) 222 | return math.sqrt(x * x + y * y) 223 | end 224 | 225 | --get the distance between two points 226 | function mathx.distance(x1, y1, x2, y2) 227 | local dx = x1 - x2 228 | local dy = y1 - y2 229 | return mathx.length(dx, dy) 230 | end 231 | 232 | return mathx 233 | -------------------------------------------------------------------------------- /measure.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | very simple benchmarking tools for finding out 3 | how long something takes to run 4 | ]] 5 | 6 | local path = (...):gsub("measure", "") 7 | local functional = require(path .. "functional") 8 | 9 | local measure = {} 10 | 11 | --replace this with whatever your highest accuracy timer is 12 | --os.time will almost certainly be too coarse 13 | measure.get_time = os.time 14 | if love and love.timer then 15 | --love.timer is _much_ better 16 | measure.get_time = love.timer.getTime 17 | end 18 | 19 | --measure the mean, minimum, and maximum time taken in seconds to run test_function 20 | --over several runs (default 1000). 21 | --warmup_runs can be provided to give the JIT or cache some time to warm up, but 22 | --are off by default 23 | function measure.time_taken(test_function, runs, warmup_runs) 24 | --defaults 25 | runs = runs or 1000 26 | warmup_runs = warmup_runs or 0 27 | --collect data 28 | local times = {} 29 | for i = 1, warmup_runs + runs do 30 | local start_time = measure.get_time() 31 | test_function() 32 | local end_time = measure.get_time() 33 | if i > warmup_runs then 34 | table.insert(times, end_time - start_time) 35 | end 36 | end 37 | 38 | local mean = functional.mean(times) 39 | local min, max = functional.minmax(times) 40 | return mean, min, max 41 | end 42 | 43 | --measure the mean, minimum, and maximum memory increase in kilobytes for a run of test_function 44 | --doesn't modify the gc state each run, to emulate normal running conditions 45 | function measure.memory_taken(test_function, runs, warmup_runs) 46 | --defaults 47 | runs = runs or 1000 48 | warmup_runs = warmup_runs or 0 49 | --collect data 50 | local mems = {} 51 | for i = 1, warmup_runs + runs do 52 | local start_mem = collectgarbage("count") 53 | test_function() 54 | local end_mem = collectgarbage("count") 55 | if i > warmup_runs then 56 | table.insert(mems, math.max(0, end_mem - start_mem)) 57 | end 58 | end 59 | 60 | local mean = functional.mean(mems) 61 | local min, max = functional.minmax(mems) 62 | return mean, min, max 63 | end 64 | 65 | --measure the mean, minimum, and maximum memory increase in kilobytes for a run of test_function 66 | --performs a full collection each run and then stops the gc, so the amount reported is as close as possible to the total amount allocated each run 67 | function measure.memory_taken_strict(test_function, runs, warmup_runs) 68 | --defaults 69 | runs = runs or 1000 70 | warmup_runs = warmup_runs or 0 71 | --collect data 72 | local mems = {} 73 | for i = 1, warmup_runs + runs do 74 | collectgarbage("collect") 75 | collectgarbage("stop") 76 | local start_mem = collectgarbage("count") 77 | test_function() 78 | local end_mem = collectgarbage("count") 79 | if i > warmup_runs then 80 | table.insert(mems, math.max(0, end_mem - start_mem)) 81 | end 82 | end 83 | collectgarbage("restart") 84 | 85 | local mean = functional.mean(mems) 86 | local min, max = functional.minmax(mems) 87 | return mean, min, max 88 | end 89 | 90 | return measure 91 | -------------------------------------------------------------------------------- /pathfind.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | general pathfinding with a general goal 3 | ]] 4 | 5 | local path = (...):gsub("pathfind", "") 6 | local sort = require(path .. "sort") 7 | 8 | --helper to generate a constant value for default weight, heuristic 9 | local generate_constant = function() return 1 end 10 | 11 | --find a path in a weighted graph 12 | -- uses A* algorithm internally 13 | --returns a table of all the nodes from the start to the goal, 14 | -- or false if no path was found 15 | --arguments table requires the following fields 16 | -- start - the value of the node to start from 17 | -- alias: start_node 18 | -- is_goal - a function which takes a node, and returns true is a goal 19 | -- alias: goal 20 | -- neighbours - a function which takes a node, and returns a table of neighbour nodes 21 | -- alias: generate_neighbours 22 | -- distance - a function which given two nodes, returns the distance between them 23 | -- defaults to a constant distance, which means all steps between nodes have the same cost 24 | -- alias: weight, g 25 | -- heuristic - a function which given a node, returns a heuristic indicative of the distance to the goal; 26 | -- can be used to speed up searches at the cost of accuracy by returning some proportion higher than the actual distance 27 | -- defaults to a constant value, which means the A* algorithm degrades to breadth-first search 28 | -- alias: h 29 | local function pathfind(args) 30 | local start = args.start or args.start_node 31 | local is_goal = args.is_goal or args.goal 32 | local neighbours = args.neighbours or args.generate_neighbours 33 | local distance = args.distance or args.weight or args.g or generate_constant 34 | local heuristic = args.heuristic or args.h or generate_constant 35 | 36 | local predecessor = {} 37 | local seen = {} 38 | local f_score = {[start] = 0} 39 | local g_score = {[start] = 0} 40 | 41 | local function search_compare(a, b) 42 | return 43 | (f_score[a] or math.huge) > 44 | (f_score[b] or math.huge) 45 | end 46 | local to_search = {} 47 | 48 | local current = start 49 | while current and not is_goal(current) do 50 | seen[current] = true 51 | for i, node in ipairs(neighbours(current)) do 52 | if not seen[node] then 53 | local tentative_g_score = (g_score[current] or math.huge) + distance(current, node) 54 | if g_score[node] == nil then 55 | if tentative_g_score < (g_score[node] or math.huge) then 56 | predecessor[node] = current 57 | g_score[node] = tentative_g_score 58 | f_score[node] = tentative_g_score + heuristic(node) 59 | end 60 | table.insert(to_search, node) 61 | sort.insertion_sort(to_search, search_compare) 62 | end 63 | end 64 | end 65 | current = table.remove(to_search) 66 | end 67 | 68 | --didn't make it to the goal 69 | if not current or not is_goal(current) then 70 | return false 71 | end 72 | 73 | --build up result path 74 | local result = {} 75 | while current do 76 | table.insert(result, 1, current) 77 | current = predecessor[current] 78 | end 79 | return result 80 | end 81 | 82 | -- Based on https://github.com/lewtds/pathfinder.lua/blob/master/pathfinder.lua 83 | -- with modification to allow for generalised goals/heuristics 84 | -- Modified 2022 Max Cahill 85 | 86 | -- Original License: 87 | -- Copyright © 2016 Trung Ngo 88 | 89 | -- Permission is hereby granted, free of charge, to any person obtaining a 90 | -- copy of this software and associated documentation files (the \"Software\"), 91 | -- to deal in the Software without restriction, including without limitation 92 | -- the rights to use, copy, modify, merge, publish, distribute, sublicense, 93 | -- and/or sell copies of the Software, and to permit persons to whom the 94 | -- Software is furnished to do so, subject to the following conditions: 95 | -- 96 | -- The above copyright notice and this permission notice shall be included in 97 | -- all copies or substantial portions of the Software. 98 | -- 99 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 100 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 101 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 102 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 103 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 104 | -- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 105 | -- DEALINGS IN THE SOFTWARE. 106 | 107 | return pathfind 108 | -------------------------------------------------------------------------------- /pretty.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | pretty formatting and printing for nested data structures 3 | 4 | also able to be parsed by lua in _many_ cases, but _not all cases_, be careful! 5 | 6 | circular references and depth limit will cause the string to contain 7 | things that cannot be parsed. 8 | 9 | this isn't a full serialisation solution, it's for debugging and display to humans 10 | 11 | all exposed functions take a config table, 12 | defaults found (and can be modified) in pretty.default_config 13 | 14 | indent 15 | indentation to use for each line, or "" for single-line packed 16 | can be a number of spaces, boolean, or a string to use verbatim 17 | depth 18 | a limit on how deep to explore the table 19 | per_line 20 | how many fields to print per line 21 | ]] 22 | 23 | local path = (...):gsub("pretty", "") 24 | local table = require(path.."tablex") --shadow global table module 25 | 26 | local pretty = {} 27 | 28 | pretty.default_config = { 29 | indent = true, 30 | depth = math.huge, 31 | per_line = 1, 32 | } 33 | 34 | --indentation to use when `indent = true` is provided 35 | pretty.default_indent = "\t" 36 | 37 | --pretty-print something directly 38 | function pretty.print(input, config) 39 | print(pretty.string(input, config)) 40 | end 41 | 42 | --pretty-format something into a string 43 | function pretty.string(input, config) 44 | return pretty._process(input, config) 45 | end 46 | 47 | --internal 48 | --actual processing part 49 | function pretty._process(input, config, processing_state) 50 | --if the input is not a table, or it has a tostring metamethod 51 | --then we can just use tostring directly 52 | local mt = getmetatable(input) 53 | if type(input) ~= "table" or mt and mt.__tostring then 54 | local s = tostring(input) 55 | --quote strings 56 | if type(input) == "string" then 57 | s = '"' .. s .. '"' 58 | end 59 | return s 60 | end 61 | 62 | --pull out config 63 | config = table.overlay({}, pretty.default_config, config or {}) 64 | 65 | local per_line = config.per_line 66 | local depth = config.depth 67 | local indent = config.indent 68 | if type(indent) == "number" then 69 | indent = (" "):rep(indent) 70 | elseif type(indent) == "boolean" then 71 | indent = indent and pretty.default_indent or "" 72 | end 73 | 74 | --dependent vars 75 | local newline = indent == "" and "" or "\n" 76 | 77 | --init or collect processing state 78 | processing_state = processing_state or { 79 | circular_references = {i = 1}, 80 | depth = 0, 81 | } 82 | 83 | processing_state.depth = processing_state.depth + 1 84 | if processing_state.depth > depth then 85 | processing_state.depth = processing_state.depth - 1 86 | return "{...}" 87 | end 88 | 89 | local circular_references = processing_state.circular_references 90 | local ref = circular_references[input] 91 | if ref then 92 | if not ref.string then 93 | ref.string = string.format("%%%d", circular_references.i) 94 | circular_references.i = circular_references.i + 1 95 | end 96 | return ref.string 97 | end 98 | ref = {} 99 | circular_references[input] = ref 100 | 101 | local function internal_value(v) 102 | v = pretty._process(v, config, processing_state) 103 | if indent ~= "" then 104 | v = v:gsub(newline, newline..indent) 105 | end 106 | return v 107 | end 108 | 109 | --otherwise, we'll build up a table representation of our data 110 | --collate into member chunks 111 | local chunks = {} 112 | --(tracking for already-seen elements from ipairs) 113 | local seen = {} 114 | --sequential part first 115 | --(in practice, pairs already does this, but the order isn't guaranteed) 116 | for i, v in ipairs(input) do 117 | seen[i] = true 118 | table.insert(chunks, internal_value(v)) 119 | end 120 | --non sequential follows 121 | for k, v in table.spairs(input) do 122 | if not seen[k] then 123 | --encapsulate anything that's not a string 124 | --todo: also keywords and strings with spaces 125 | if type(k) ~= "string" then 126 | k = "[" .. tostring(k) .. "]" 127 | end 128 | table.insert(chunks, k .. " = " .. internal_value(v)) 129 | end 130 | end 131 | 132 | --resolve number to newline skip after 133 | if per_line > 1 then 134 | local line_chunks = {} 135 | while #chunks > 0 do 136 | local break_next = false 137 | local line = {} 138 | for i = 1, per_line do 139 | if #chunks == 0 then 140 | break 141 | end 142 | local v = chunks[1] 143 | --tables split to own line 144 | if v:find("{") then 145 | --break line here 146 | break_next = true 147 | break 148 | else 149 | table.insert(line, table.remove(chunks, 1)) 150 | end 151 | end 152 | if #line > 0 then 153 | table.insert(line_chunks, table.concat(line, ", ")) 154 | end 155 | if break_next then 156 | table.insert(line_chunks, table.remove(chunks, 1)) 157 | end 158 | end 159 | chunks = line_chunks 160 | end 161 | 162 | --drop depth 163 | processing_state.depth = processing_state.depth - 1 164 | 165 | --remove circular 166 | circular_references[input] = nil 167 | 168 | local multiline = #chunks > 1 169 | local separator = (indent == "" or not multiline) and ", " or ",\n"..indent 170 | 171 | local prelude = ref.string and (string.format(" ",ref.string)) or "" 172 | if multiline then 173 | return "{" .. prelude .. newline .. 174 | indent .. table.concat(chunks, separator) .. newline .. 175 | "}" 176 | end 177 | return "{" .. prelude .. table.concat(chunks, separator) .. "}" 178 | end 179 | 180 | return pretty 181 | -------------------------------------------------------------------------------- /pubsub.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | dead-simple publish-subscribe message bus 3 | ]] 4 | 5 | local path = (...):gsub("pubsub", "") 6 | local class = require(path .. "class") 7 | local set = require(path .. "set") 8 | local tablex = require(path .. "tablex") 9 | 10 | local pubsub = class({ 11 | name = "pubsub", 12 | }) 13 | 14 | --create a new pubsub bus 15 | function pubsub:new() 16 | self.subscriptions = {} 17 | self._defer = {} 18 | self._defer_stack = 0 19 | end 20 | 21 | --(internal; deferred area check) 22 | function pubsub:_deferred() 23 | return self._defer_stack > 0 24 | end 25 | 26 | --(internal; enter deferred area) 27 | function pubsub:_push_defer(event) 28 | self._defer_stack = self._defer_stack + 1 29 | if self._defer_stack > 255 then 30 | error("pubsub defer stack overflow; event infinite loop with event: "..tostring(event)) 31 | end 32 | end 33 | 34 | --(internal; enter deferred area) 35 | function pubsub:_defer_call(defer_f, event, callback) 36 | if not self:_deferred() then 37 | error("attempt to defer pubsub call when not required") 38 | end 39 | table.insert(self._defer, defer_f) 40 | table.insert(self._defer, event) 41 | table.insert(self._defer, callback) 42 | end 43 | 44 | --(internal; unwind deferred sub/unsub) 45 | function pubsub:_pop_defer(event) 46 | self._defer_stack = self._defer_stack - 1 47 | if self._defer_stack < 0 then 48 | error("pubsub defer stack underflow; don't call the defer methods directly - event reported: "..tostring(event)) 49 | end 50 | if self._defer_stack == 0 then 51 | local defer_len = #self._defer 52 | if defer_len then 53 | for i = 1, defer_len, 3 do 54 | local defer_f = self._defer[i] 55 | local defer_event = self._defer[i+1] 56 | local defer_cb = self._defer[i+2] 57 | self[defer_f](self, defer_event, defer_cb) 58 | end 59 | tablex.clear(self._defer) 60 | end 61 | end 62 | end 63 | 64 | --(internal; notify a callback set of an event) 65 | function pubsub:_notify(event, callbacks, ...) 66 | if callbacks then 67 | self:_push_defer(event) 68 | for _, f in ipairs(callbacks:values()) do 69 | f(...) 70 | end 71 | self:_pop_defer(event) 72 | end 73 | end 74 | 75 | --publish an event, with optional arguments 76 | --notifies both the direct subscribers, and those subscribed to "everything" 77 | function pubsub:publish(event, ...) 78 | self:_notify(event, self.subscriptions[event], ...) 79 | self:_notify(event, self.subscriptions.everything, event, ...) 80 | end 81 | 82 | --subscribe to an event 83 | --can be a specifically named event, or "everything" to get notified for any event 84 | --for "everything", the callback will receive the event name as the first argument 85 | function pubsub:subscribe(event, callback) 86 | if self:_deferred() then 87 | self:_defer_call("subscribe", event, callback) 88 | return 89 | end 90 | local callbacks = self.subscriptions[event] 91 | if not callbacks then 92 | callbacks = set() 93 | self.subscriptions[event] = callbacks 94 | end 95 | callbacks:add(callback) 96 | end 97 | 98 | --subscribe to an event, automatically unsubscribe once called 99 | --return the function that can be used to unsubscribe early if needed 100 | function pubsub:subscribe_once(event, callback) 101 | local f 102 | local called = false 103 | f = function(...) 104 | if not called then 105 | callback(...) 106 | self:unsubscribe(event, f) 107 | called = true 108 | end 109 | end 110 | self:subscribe(event, f) 111 | return f 112 | end 113 | 114 | --unsubscribe from an event 115 | function pubsub:unsubscribe(event, callback) 116 | if self:_deferred() then 117 | self:_defer_call("unsubscribe", event, callback) 118 | return 119 | end 120 | local callbacks = self.subscriptions[event] 121 | if callbacks then 122 | callbacks:remove(callback) 123 | if callbacks:size() == 0 then 124 | self.subscriptions[event] = nil 125 | end 126 | end 127 | end 128 | 129 | --check if there is a subscriber for a given event 130 | function pubsub:has_subscriber(event) 131 | return self.subscriptions[event] ~= nil 132 | end 133 | 134 | return pubsub 135 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ⚡ Batteries for Lua 2 | 3 | > Helpful stuff for making games with lua, especially with [löve](https://love2d.org). 4 | 5 | Get your projects off the ground faster! `batteries` fills out lua's sparse standard library, and provides implementations of many common algorithms and data structures useful for games. 6 | 7 | General purpose and special case, extensively documented in-line, and around a hundred kilobytes uncompressed - including the license and this readme - so you get quite a lot per byte! Of course, feel free to trim it down for your use case as required (see [below](#stripping-down-batteries)). 8 | 9 | # Getting Started 10 | 11 | ## How does `that module` work? 12 | 13 | Examples are [in another repo](https://github.com/1bardesign/batteries-examples) to avoid cluttering the repo history and your project/filesystem when used as a submodule. 14 | 15 | They have short, straightforward usage examples of much of the provided functionality. 16 | 17 | Documentation is provided as comments alongside the code, because that is the most resilient place for it. Even auto-generated docs often end up out of date and the annotations bloat the code. 18 | 19 | Use find-in-all from your editor, or just browse through the code. The [module overview](#module-overview) below is the only non-code documentation - it is a jumping off point, not a reference. 20 | 21 | ## Installation 22 | 23 | `batteries` works straight out of the repo with no separate build step. The license file required for use is included. 24 | 25 | - Put the files in their own directory, somewhere your project can access them. 26 | - `require` the base `batteries` directory - the one with `init.lua` in it. 27 | - Don't forget to use dot syntax on the path! 28 | - With a normal `require` setup (ie stock LÖVE or lua), `init.lua` will pull in all the submodules. 29 | - Batteries uses the (very common) init.lua convention. If your installation doesn't already have init.lua support (eg plain 5.1 on windows), add `package.path = package.path .. ";./?/init.lua"` before the require line. You can also modify your `LUA_PATH` environment variable. 30 | - (optionally) `export` everything to the global environment. 31 | 32 | ```lua 33 | --everything as globals 34 | require("path.to.batteries"):export() 35 | 36 | -- OR -- 37 | 38 | --self-contained 39 | local batteries = require("path.to.batteries") 40 | ``` 41 | 42 | See [below](#export-globals) for a discussion of the pros and cons of `export`. 43 | 44 | # Library Culture and Contributing 45 | 46 | `batteries` aims to be approachable to almost everyone, but I _do_ expect you to get your hands dirty. I'm very open to collaboration, and happy to talk through issues or shortcomings in good faith. 47 | 48 | Pull requests are welcome for anything - positive changes will be merged optimistically, and I'm happy to work with you to get anything sensible ready for inclusion. 49 | 50 | If you have something "big" to contribute _please_ get in touch before starting work so we can make sure it fits. I'm quite open minded! 51 | 52 | If you've had a good look for the answer but something remains unclear, raise an issue and I'll address it. If you _haven't_ had a good look for the answer, checking the source _always_ helps! 53 | 54 | If you'd prefer to talk with me about `batteries` in real time, I'm often available on the love2d discord. 55 | 56 | # Module Overview 57 | 58 | **Lua Core Extensions:** 59 | 60 | Extensions to existing lua core modules to provide missing features. 61 | 62 | - [`mathx`](./mathx.lua) - Mathematical extensions. Alias `math`. 63 | - [`tablex`](./tablex.lua) - Table handling extensions. Alias `table`. 64 | - [`stringx`](./stringx.lua) - String handling extensions. Alias `string`. 65 | 66 | **General Utility:** 67 | 68 | General utility data structures and algorithms to speed you along your way. 69 | 70 | - [`class`](./class.lua) - OOP with inheritance and interfaces in a single function. 71 | - [`functional`](./functional.lua) - Functional programming facilities. `map`, `reduce`, `any`, `match`, `minmax`, `mean`... 72 | - [`sequence`](./sequence.lua) - An oo wrapper on sequential tables, so you can do `t:insert(i, v)` instead of `table.insert(t, i, v)`. Also supports method chaining for the `functional` interface above, which can save a lot of needless typing! 73 | - [`set`](./set.lua) - A set type supporting a full suite of set operations with fast membership testing and `ipairs`-style iteration. 74 | - [`sort`](./sort.lua) - Provides a stable merge+insertion sorting algorithm that is also, as a bonus, often faster than `table.sort` under luajit. Also exposes `insertion_sort` if needed. Alias `stable_sort`. 75 | - [`state_machine`](./state_machine.lua) - Finite state machine implementation with state transitions and all the rest. Useful for game states, AI, cutscenes... 76 | - [`timer`](./timer.lua) - a "countdown" style timer with progress and completion callbacks. 77 | - [`pubsub`](./pubsub.lua) - a self-contained publish/subscribe message bus. Immediate mode rather than queued, local rather than networked, but if you were expecting mqtt in 60 lines I don't know what to tell you. Scales pretty well nonetheless. 78 | - [`pretty`](./pretty.lua) - pretty printing tables for debug inspection. 79 | 80 | **Geometry:** 81 | 82 | Modules to help work with spatial concepts. 83 | 84 | - [`intersect`](./intersect.lua) - 2d intersection routines, a bit sparse at the moment. 85 | - [`vec2`](./vec2.lua) - 2d vectors with method chaining, and garbage saving modifying operations. A bit of a mouthful at times, but you get used to it. (there's an issue discussing future solutions). 86 | - [`vec3`](./vec3.lua) - 3d vectors as above. 87 | 88 | **Special Interest:** 89 | 90 | These modules are probably only useful to some folks in some circumstances, or are under-polished for one reason or another. 91 | 92 | - [`async`](./async.lua) - Asynchronous/"Background" task management. 93 | - [`colour`](./colour.lua) - Colour conversion routines. Alias `color`. 94 | - [`manual_gc`](./manual_gc.lua) - Get GC out of your update/draw calls. Useful when trying to get accurate profiling information; moves "randomness" of GC. Requires you to think a bit about your garbage budgets though. 95 | - [`measure`](./measure.lua) - Benchmarking helpers - measure the time or memory taken to run some code. 96 | - [`make_pooled`](./make_pooled.lua) - add pooling/recycling capability to a class 97 | 98 | Any aliases are provided at both the `batteries` module level, and globally when exported. 99 | 100 | # Work in Progress, or TODO 101 | 102 | Endless, of course :) 103 | 104 | - `colour` - Bidirectional hsv/hsl/etc conversion would fit nicely here. 105 | - Geometry: 106 | - `vec3` - Needs more fleshing out for serious use, and a refactor to fit the same naming patterns as `vec2`. 107 | - `matrix` - A geometry focussed matrix module would made 3d work a lot nicer. Possibly just `mat4`. 108 | - `intersect` - More routines, more optimisation :) 109 | - Network: 110 | - Various helpers for networked systems, game focus of course. 111 | - `rpc` - Remote procedure call system on top of `enet` or `socket` or both. 112 | - `delta` - Detect and sync changes to objects. 113 | - Broadphase: 114 | - Spatial simplification systems for different needs. Probably AABB or point insertion of data. 115 | - `bucket_grid` - Dumb 2d bucket broadphase. 116 | - `sweep_and_prune` - Popular for bullet hell games. 117 | - `quadtree`/`octree` - Everyone's favourite ;) 118 | - UI 119 | - Maybe adopt [partner](https://github.com/1bardesign/partner) in here, or something evolved from it. 120 | - Image 121 | - Maybe adopt [chromatic](https://github.com/1bardesign/chromatic) in here, or something evolved from it. 122 | 123 | # FAQ 124 | 125 | ## Export Globals 126 | 127 | You are strongly encouraged to use the library in a "fire and forget" manner through `require("batteries"):export()` (or whatever appropriate module path), which will modify builtin lua modules (such as `table` and `math`), and expose all the other modules directly as globals for your convenience. 128 | 129 | This eases consumption across your project - you don't have to require modules everywhere, or remember if say, `table.remove_value` is built in to lua or not, or get used to accessing the builtin table functions through `batteries.table` or `tablex`. 130 | 131 | While this will likely sit badly with anyone who's had "no globals! ever!" hammered into them, I believe that for `batteries` (and many foundational libraries) it makes sense to just import once at boot. You're going to be pulling it in almost everywhere anyway; why bother making yourself jump through more hoops? 132 | 133 | You can, of course, use the separate modules on their own, either requiring individual modules explicitly, or a single require for all of `batteries` and use through something like `batteries.functional.map`. This more involved approach _will_ let you be more clear about your dependencies, if you care deeply about that - at the cost of more setup work needing to re-require batteries everywhere you use it, or expose it as a global in the first place. 134 | 135 | I'd strongly recommend that if you find yourself frustrated with the above, stop and think why/if you really want to avoid globals for something intended to be commonly used across your entire codebase! Are you explicitly `require`ing `math` and `table` everywhere you use it too? Are you just as ideologically opposed to `require` being a global? 136 | 137 | You may wish to reconsider, and save yourself typing `batteries` a few hundred times :) 138 | 139 | ## Git Submodule or Static Install? 140 | 141 | `batteries` is fairly easily used as a git submodule - this is how I use it in my own projects, because updating is as quick and easy as a `git pull`, and it's easy to roll back changes if needed, and to contribute changes back upstream. 142 | 143 | A static install is harder to update, but easier to trim down if you only need some of the functionality provided. It can also _never_ mysteriously break when updating, which might be appealing to those who just cant stop themselves using the latest and greatest. 144 | 145 | ## Stripping down `batteries` 146 | 147 | Many of the modules "just work" on their own, if you just want to grab something specific. 148 | 149 | Some of them depend on `class`, which can be included alongside pretty easily. 150 | 151 | There are some other inter-dependencies in the larger modules, which should be straightforward to detect and figure out the best course of action (either include the dependency or strip out dependent functionality), if you want to make a stripped-down version for your specific use case. 152 | 153 | Currently (july 2021) the lib is 40kb or so compressed, including this readme, so do think carefully whether you really need to worry about it! 154 | 155 | ## Versioning? 156 | 157 | Currently, the library is operated in a rolling-release manner - the head of the master branch is intended for public consumption. While this is kept as stable as practical, breaking API changes _do_ happen, and more are planned! 158 | 159 | For this reason, you should try to check the commit history for what has changed rather than blindly updating. If you let me know that you're using it actively, I'm generally happy to let you know when something breaking is on its way to `master` as well. 160 | 161 | If there is a large enough user base in the future to make a versioning scheme + non-repo changelog make sense, I will accomodate. 162 | 163 | ## snake_case? Why? 164 | 165 | I personally prefer it, but I accept that it's a matter of taste and puts some people off. 166 | 167 | I've implemented experimental automatic API conversion (UpperCamelCase for types, lowerCamelCase for methods) that you can opt in to by calling `:camelCase()` before `:export()`, let me know if you use it and encounter any issues. 168 | 169 | # License 170 | 171 | zlib, see [here](license.txt) 172 | -------------------------------------------------------------------------------- /sequence.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | sequence - functional + oo wrapper for ordered tables 3 | 4 | mainly beneficial when used for method chaining 5 | to save on typing and data plumbing 6 | ]] 7 | 8 | local path = (...):gsub("sequence", "") 9 | local table = require(path .. "tablex") --shadow global table module 10 | local functional = require(path .. "functional") 11 | local stable_sort = require(path .. "sort").stable_sort 12 | 13 | --(not a class, because we want to be able to upgrade tables that are passed in without a copy) 14 | local sequence = {} 15 | sequence.__index = sequence 16 | setmetatable(sequence, { 17 | __index = table, 18 | __call = function(self, ...) 19 | return sequence:new(...) 20 | end, 21 | }) 22 | 23 | --iterators as method calls 24 | --(no pairs, sequences are ordered) 25 | --todo: pico8 like `all` 26 | sequence.ipairs = ipairs 27 | sequence.iterate = ipairs 28 | 29 | --upgrade a table into a sequence, or create a new sequence 30 | function sequence:new(t) 31 | return setmetatable(t or {}, sequence) 32 | end 33 | 34 | --sorting default to stable 35 | sequence.sort = stable_sort 36 | 37 | --patch various interfaces in a type-preserving way, for method chaining 38 | 39 | --import copying tablex 40 | --(common case where something returns another sequence for chaining) 41 | for _, v in ipairs({ 42 | "keys", 43 | "values", 44 | "dedupe", 45 | "collapse", 46 | "append", 47 | "shallow_overlay", 48 | "deep_overlay", 49 | "copy", 50 | "shallow_copy", 51 | "deep_copy", 52 | }) do 53 | local table_f = table[v] 54 | sequence[v] = function(self, ...) 55 | return sequence(table_f(self, ...)) 56 | end 57 | end 58 | 59 | --aliases 60 | for _, v in ipairs({ 61 | {"flatten", "collapse"}, 62 | }) do 63 | sequence[v[1]] = sequence[v[2]] 64 | end 65 | 66 | --import functional interface in method form 67 | 68 | --(common case where something returns another sequence for chaining) 69 | for _, v in ipairs({ 70 | "map", 71 | "map_field", 72 | "map_call", 73 | "filter", 74 | "remove_if", 75 | "zip", 76 | "combine", 77 | "stitch", 78 | "map_stitch", 79 | "cycle", 80 | "map_cycle", 81 | "chain", 82 | "map_chain", 83 | "map_pairs", 84 | }) do 85 | local functional_f = functional[v] 86 | sequence[v] = function(self, ...) 87 | return sequence(functional_f(self, ...)) 88 | end 89 | end 90 | 91 | --(cases where we don't want to construct a new sequence) 92 | for _, v in ipairs({ 93 | "map_inplace", 94 | "filter_inplace", 95 | "foreach", 96 | "reduce", 97 | "any", 98 | "none", 99 | "all", 100 | "count", 101 | "contains", 102 | "sum", 103 | "mean", 104 | "minmax", 105 | "max", 106 | "min", 107 | "find_min", 108 | "find_max", 109 | "find_nearest", 110 | "find_match", 111 | }) do 112 | sequence[v] = functional[v] 113 | end 114 | 115 | 116 | --aliases 117 | for _, v in ipairs({ 118 | {"remap", "map_inplace"}, 119 | {"map_stitch", "stitch"}, 120 | {"map_cycle", "cycle"}, 121 | {"find_best", "find_max"}, 122 | }) do 123 | sequence[v[1]] = sequence[v[2]] 124 | end 125 | 126 | --(anything that needs bespoke wrapping) 127 | function sequence:partition(f) 128 | local a, b = functional.partition(self, f) 129 | return sequence(a), sequence(b) 130 | end 131 | 132 | function sequence:unzip(f) 133 | local a, b = functional.unzip(self, f) 134 | return sequence(a), sequence(b) 135 | end 136 | 137 | return sequence 138 | -------------------------------------------------------------------------------- /set.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | set type with appropriate operations 3 | 4 | NOTE: This is actually a unique list (ordered set). So it's more than just 5 | a table with keys for values. 6 | ]] 7 | 8 | local path = (...):gsub("set", "") 9 | local class = require(path .. "class") 10 | local table = require(path .. "tablex") --shadow global table module 11 | 12 | local set = class({ 13 | name = "set", 14 | }) 15 | 16 | --construct a new set 17 | --elements is an optional ordered table of elements to be added to the set 18 | function set:new(elements) 19 | self._keyed = {} 20 | self._ordered = {} 21 | if elements then 22 | for _, v in ipairs(elements) do 23 | self:add(v) 24 | end 25 | end 26 | end 27 | 28 | --check if an element is present in the set 29 | function set:has(v) 30 | return self._keyed[v] or false 31 | end 32 | 33 | --add a value to the set, if it's not already present 34 | function set:add(v) 35 | if not self:has(v) then 36 | self._keyed[v] = true 37 | table.insert(self._ordered, v) 38 | end 39 | return self 40 | end 41 | 42 | --remove a value from the set, if it's present 43 | function set:remove(v) 44 | if self:has(v) then 45 | self._keyed[v] = nil 46 | table.remove_value(self._ordered, v) 47 | end 48 | return self 49 | end 50 | 51 | --remove all elements from the set 52 | function set:clear() 53 | if table.clear then 54 | table.clear(self._keyed) 55 | table.clear(self._ordered) 56 | else 57 | self._keyed = {} 58 | self._ordered = {} 59 | end 60 | end 61 | 62 | --get the number of distinct values in the set 63 | function set:size() 64 | return #self._ordered 65 | end 66 | 67 | --return a value from the set 68 | --index must be between 1 and size() inclusive 69 | --adding/removing invalidates indices 70 | function set:get(index) 71 | return self._ordered[index] 72 | end 73 | 74 | --iterate the values in the set, along with their index 75 | --the index is useless but harmless, and adding a custom iterator seems 76 | --like a really easy way to encourage people to use slower-than-optimal code 77 | function set:ipairs() 78 | return ipairs(self._ordered) 79 | end 80 | 81 | --get a copy of the values in the set, as a simple table 82 | function set:values() 83 | return table.shallow_copy(self._ordered) 84 | end 85 | 86 | --get a direct reference to the internal list of values in the set 87 | --do NOT modify the result, or you'll break the set! 88 | --for read-only access it avoids a needless table copy 89 | --(eg this is sensible to pass to functional apis) 90 | function set:values_readonly() 91 | return self._ordered 92 | end 93 | 94 | --convert to an ordered table, destroying set-like properties 95 | --and deliberately disabling the initial set object 96 | function set:to_table() 97 | local r = self._ordered 98 | self._ordered = nil 99 | self._keyed = nil 100 | return r 101 | end 102 | 103 | --modifying operations 104 | 105 | --add all the elements present in the other set 106 | function set:add_set(other) 107 | for i, v in other:ipairs() do 108 | self:add(v) 109 | end 110 | return self 111 | end 112 | 113 | --remove all the elements present in the other set 114 | function set:subtract_set(other) 115 | for i, v in other:ipairs() do 116 | self:remove(v) 117 | end 118 | return self 119 | end 120 | 121 | --new collection operations 122 | 123 | --copy a set 124 | function set:copy() 125 | return set():add_set(self) 126 | end 127 | 128 | --create a new set containing the complement of the other set contained in this one 129 | --the elements present in this set but not present in the other set will remain in the result 130 | function set:complement(other) 131 | return self:copy():subtract_set(other) 132 | end 133 | 134 | --alias 135 | set.difference = set.complement 136 | 137 | --create a new set containing the union of this set with another 138 | --an element present in either set will be present in the result 139 | function set:union(other) 140 | return self:copy():add_set(other) 141 | end 142 | 143 | --create a new set containing the intersection of this set with another 144 | --only the elements present in both sets will remain in the result 145 | function set:intersection(other) 146 | local r = set() 147 | for i, v in self:ipairs() do 148 | if other:has(v) then 149 | r:add(v) 150 | end 151 | end 152 | return r 153 | end 154 | 155 | --create a new set containing the symmetric difference of this set with another 156 | --only the elements not present in both sets will remain in the result 157 | --similiar to a logical XOR operation 158 | -- 159 | --equal to self:union(other):subtract_set(self:intersection(other)) 160 | -- but with much less wasted effort 161 | function set:symmetric_difference(other) 162 | local r = set() 163 | for i, v in self:ipairs() do 164 | if not other:has(v) then 165 | r:add(v) 166 | end 167 | end 168 | for i, v in other:ipairs() do 169 | if not self:has(v) then 170 | r:add(v) 171 | end 172 | end 173 | return r 174 | end 175 | 176 | --alias 177 | set.xor = set.symmetric_difference 178 | 179 | -- 180 | return set 181 | -------------------------------------------------------------------------------- /sort.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | various sorting routines 3 | ]] 4 | 5 | --this is based on code from Dirk Laurie and Steve Fisher, 6 | --used under license as follows: 7 | 8 | --[[ 9 | Copyright © 2013 Dirk Laurie and Steve Fisher. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a 12 | copy of this software and associated documentation files (the "Software"), 13 | to deal in the Software without restriction, including without limitation 14 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | and/or sell copies of the Software, and to permit persons to whom the 16 | Software is furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | ]] 29 | 30 | -- (modifications by Max Cahill 2018, 2020) 31 | 32 | local sort = {} 33 | 34 | --tunable size for insertion sort "bottom out" 35 | sort.max_chunk_size = 32 36 | 37 | --insertion sort on a section of array 38 | function sort._insertion_sort_impl(array, first, last, less) 39 | for i = first + 1, last do 40 | local k = first 41 | local v = array[i] 42 | for j = i, first + 1, -1 do 43 | if less(v, array[j - 1]) then 44 | array[j] = array[j - 1] 45 | else 46 | k = j 47 | break 48 | end 49 | end 50 | array[k] = v 51 | end 52 | end 53 | 54 | --merge sorted adjacent sections of array 55 | function sort._merge(array, workspace, low, middle, high, less) 56 | local i, j, k 57 | i = 1 58 | -- copy first half of array to auxiliary array 59 | for w = low, middle do 60 | workspace[i] = array[w] 61 | i = i + 1 62 | end 63 | -- sieve through 64 | i = 1 65 | j = middle + 1 66 | k = low 67 | while true do 68 | if (k >= j) or (j > high) then 69 | break 70 | end 71 | if less(array[j], workspace[i]) then 72 | array[k] = array[j] 73 | j = j + 1 74 | else 75 | array[k] = workspace[i] 76 | i = i + 1 77 | end 78 | k = k + 1 79 | end 80 | -- copy back any remaining elements of first half 81 | for w = k, j - 1 do 82 | array[w] = workspace[i] 83 | i = i + 1 84 | end 85 | end 86 | 87 | --implementation for the merge sort 88 | function sort._merge_sort_impl(array, workspace, low, high, less) 89 | if high - low <= sort.max_chunk_size then 90 | sort._insertion_sort_impl(array, low, high, less) 91 | else 92 | local middle = math.floor((low + high) / 2) 93 | sort._merge_sort_impl(array, workspace, low, middle, less) 94 | sort._merge_sort_impl(array, workspace, middle + 1, high, less) 95 | sort._merge(array, workspace, low, middle, high, less) 96 | end 97 | end 98 | 99 | --default comparison; hoisted for clarity 100 | local _sorted_types = { 101 | --a list of types that will be sorted by default_less 102 | --provide a custom sort function to sort other types 103 | ["number"] = 1, 104 | ["string"] = 2, 105 | } 106 | local function default_less(a, b) 107 | local sort_a = _sorted_types[type(a)] 108 | local sort_b = _sorted_types[type(b)] 109 | if not sort_a or not sort_b then 110 | return false 111 | end 112 | --different types, sorted by type 113 | if sort_a ~= sort_b then 114 | return sort_a < sort_b 115 | end 116 | --otherwise same type, use less 117 | return a < b 118 | end 119 | 120 | --export it so others can use it 121 | sort.default_less = default_less 122 | 123 | --inline common setup stuff 124 | function sort._sort_setup(array, less) 125 | --default less 126 | less = less or default_less 127 | -- 128 | local n = #array 129 | --trivial cases; empty or 1 element 130 | local trivial = (n <= 1) 131 | if not trivial then 132 | --check less 133 | if less(array[1], array[1]) then 134 | error("invalid order function for sorting; less(v, v) should not be true for any v.") 135 | end 136 | end 137 | --setup complete 138 | return trivial, n, less 139 | end 140 | 141 | function sort.stable_sort(array, less) 142 | --setup 143 | local trivial, n 144 | trivial, n, less = sort._sort_setup(array, less) 145 | if not trivial then 146 | --temp storage; allocate ahead of time 147 | local workspace = {} 148 | local middle = math.ceil(n / 2) 149 | workspace[middle] = array[1] 150 | --dive in 151 | sort._merge_sort_impl( array, workspace, 1, n, less ) 152 | end 153 | return array 154 | end 155 | 156 | function sort.insertion_sort(array, less) 157 | --setup 158 | local trivial, n 159 | trivial, n, less = sort._sort_setup(array, less) 160 | if not trivial then 161 | sort._insertion_sort_impl(array, 1, n, less) 162 | end 163 | return array 164 | end 165 | 166 | sort.unstable_sort = table.sort 167 | 168 | --export sort core to the global table module 169 | function sort:export() 170 | table.insertion_sort = sort.insertion_sort 171 | table.stable_sort = sort.stable_sort 172 | table.unstable_sort = sort.unstable_sort 173 | end 174 | 175 | return sort 176 | -------------------------------------------------------------------------------- /state_machine.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | state machine 3 | 4 | a finite state machine implementation 5 | 6 | each state is either: 7 | 8 | - a table with enter, exit, update and draw callbacks (all optional) 9 | which each take the state table and varargs as arguments 10 | - a plain function 11 | which gets passed the current event name, the machine table, and varargs as arguments 12 | 13 | on changing state, the outgoing state's exit callback is called 14 | then the incoming state's enter callback is called 15 | enter can trigger another transition by returning a string 16 | 17 | on update, the current state's update callback is called 18 | the return value can trigger a transition 19 | 20 | on draw, the current state's draw callback is called 21 | the return value is discarded 22 | 23 | TODO: consider coroutine friendliness 24 | ]] 25 | 26 | local path = (...):gsub("state_machine", "") 27 | local class = require(path .. "class") 28 | 29 | local state_machine = class({ 30 | name = "state_machine", 31 | }) 32 | 33 | function state_machine:new(states, start_in_state) 34 | self.states = states or {} 35 | self.current_state_name = "" 36 | self.prev_state_name = "" 37 | self.reset_state_name = start_in_state or "" 38 | self:reset() 39 | end 40 | 41 | --get the current state table (or nil if it doesn't exist) 42 | function state_machine:current_state() 43 | return self.states[self.current_state_name] 44 | end 45 | 46 | ------------------------------------------------------------------------------- 47 | --internal helpers 48 | 49 | --make an internal call 50 | function state_machine:_call(name, ...) 51 | local state = self:current_state() 52 | if state then 53 | if type(state[name]) == "function" then 54 | return state[name](state, ...) 55 | elseif type(state) == "function" then 56 | return state(name, self, ...) 57 | end 58 | end 59 | return nil 60 | end 61 | 62 | --make an internal call 63 | -- transition if the return value is a valid state name - and return nil if so 64 | -- return the call result if it isn't a valid state name 65 | function state_machine:_call_and_transition(name, ...) 66 | local r = self:_call(name, ...) 67 | if type(r) == "string" and self:has_state(r) then 68 | self:set_state(r, true) 69 | return nil 70 | end 71 | return r 72 | end 73 | 74 | ------------------------------------------------------------------------------- 75 | --various checks 76 | 77 | function state_machine:in_state(name) 78 | return self.current_state_name == name 79 | end 80 | 81 | function state_machine:has_state(name) 82 | return self.states[name] ~= nil 83 | end 84 | 85 | ------------------------------------------------------------------------------- 86 | --state management 87 | 88 | --add a state 89 | function state_machine:add_state(name, state) 90 | if self:has_state(name) then 91 | error("error: added duplicate state " .. name) 92 | else 93 | self.states[name] = state 94 | if self:in_state(name) then 95 | self:_call_and_transition("enter") 96 | end 97 | end 98 | 99 | return self 100 | end 101 | 102 | --remove a state 103 | function state_machine:remove_state(name) 104 | if not self:has_state(name) then 105 | error("error: removed missing state " .. name) 106 | else 107 | if self:in_state(name) then 108 | self:_call("exit") 109 | end 110 | self.states[name] = nil 111 | end 112 | 113 | return self 114 | end 115 | 116 | --hard-replace a state table 117 | -- if we're replacing the current state, 118 | -- exit is called on the old state and enter is called on the new state 119 | -- mask_transitions can be used to prevent this if you need to 120 | function state_machine:replace_state(name, state, mask_transitions) 121 | local do_transitions = not mask_transitions and self:in_state(name) 122 | if do_transitions then 123 | self:_call("exit") 124 | end 125 | self.states[name] = state 126 | if do_transitions then 127 | self:_call_and_transition("enter", self) 128 | end 129 | 130 | return self 131 | end 132 | 133 | --ensure a state doesn't exist; transition out of it if we're currently in it 134 | function state_machine:clear_state(name) 135 | return self:replace_state(name, nil) 136 | end 137 | 138 | ------------------------------------------------------------------------------- 139 | --transitions and updates 140 | 141 | --reset the machine state to whatever state was specified at creation 142 | function state_machine:reset() 143 | if self.reset_state_name then 144 | self:set_state(self.reset_state_name, true) 145 | end 146 | end 147 | 148 | --set the current state 149 | -- if the enter callback of the target state returns a valid state name, 150 | -- then it is transitioned to in turn, 151 | -- and so on until the machine is at rest 152 | function state_machine:set_state(name, reset) 153 | if self.current_state_name ~= name or reset then 154 | self:_call("exit") 155 | self.prev_state_name = self.current_state_name 156 | self.current_state_name = name 157 | self:_call_and_transition("enter", self) 158 | end 159 | return self 160 | end 161 | 162 | --perform an update 163 | --pass in an optional delta time, which is passed as an arg to the state functions 164 | --if the state update returns a string, and we have that state 165 | -- then we change state (reset if it's the current state) 166 | -- and return nil 167 | --otherwise, the result is returned 168 | function state_machine:update(dt) 169 | return self:_call_and_transition("update", dt) 170 | end 171 | 172 | --draw the current state 173 | function state_machine:draw() 174 | self:_call("draw") 175 | end 176 | 177 | --for compatibility when a state machine is nested as a state in another machine 178 | function state_machine:enter(parent) 179 | self.parent = parent 180 | self:reset() 181 | end 182 | 183 | return state_machine 184 | -------------------------------------------------------------------------------- /stringx.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | extra string routines 3 | ]] 4 | 5 | local path = (...):gsub("stringx", "") 6 | local assert = require(path .. "assert") 7 | local pretty = require(path .. "pretty") 8 | 9 | local stringx = setmetatable({}, { 10 | __index = string 11 | }) 12 | 13 | --split a string on a delimiter into an ordered table 14 | function stringx.split(self, delim, limit) 15 | delim = delim or "" 16 | limit = (limit ~= nil and limit) or math.huge 17 | 18 | assert:type(self, "string", "stringx.split - self", 1) 19 | assert:type(delim, "string", "stringx.split - delim", 1) 20 | assert:type(limit, "number", "stringx.split - limit", 1) 21 | 22 | if limit then 23 | assert(limit >= 0, "max_split must be positive!") 24 | end 25 | 26 | --we try to create as little garbage as possible! 27 | --only one table to contain the result, plus the split strings. 28 | --so we do two passes, and work with the bytes underlying the string 29 | --partly because string.find is not compiled on older luajit :) 30 | local res = {} 31 | local length = self:len() 32 | -- 33 | local delim_length = delim:len() 34 | --empty delim? split to individual characters 35 | if delim_length == 0 then 36 | for i = 1, length do 37 | table.insert(res, self:sub(i, i)) 38 | end 39 | return res 40 | end 41 | local delim_start = delim:byte(1) 42 | --pass 1 43 | --collect split sites 44 | local i = 1 45 | while i <= length do 46 | --scan for delimiter 47 | if self:byte(i) == delim_start then 48 | local has_whole_delim = true 49 | for j = 2, delim_length do 50 | if self:byte(i + j - 1) ~= delim:byte(j) then 51 | has_whole_delim = false 52 | --step forward as far as we got 53 | i = i + j 54 | break 55 | end 56 | end 57 | if has_whole_delim then 58 | if #res < limit then 59 | table.insert(res, i) 60 | --iterate forward the whole delimiter 61 | i = i + delim_length 62 | else 63 | break 64 | end 65 | end 66 | else 67 | --iterate forward 68 | i = i + 1 69 | end 70 | end 71 | --pass 2 72 | --collect substrings 73 | i = 1 74 | for si, j in ipairs(res) do 75 | res[si] = self:sub(i, j-1) 76 | i = j + delim_length 77 | end 78 | --add the final section 79 | table.insert(res, self:sub(i, -1)) 80 | --return the collection 81 | return res 82 | end 83 | 84 | stringx.pretty = pretty.string 85 | 86 | --(generate a map of whitespace byte values) 87 | local _whitespace_bytes = {} 88 | do 89 | local _whitespace = " \t\n\r" 90 | for i = 1, _whitespace:len() do 91 | _whitespace_bytes[_whitespace:byte(i)] = true 92 | end 93 | end 94 | 95 | --trim all whitespace off the head and tail of a string 96 | -- specifically trims space, tab, newline, and carriage return characters 97 | -- ignores form feeds, vertical tabs, and backspaces 98 | -- 99 | -- only generates one string of garbage in the case there's actually space to trim 100 | function stringx.trim(s) 101 | --cache 102 | local len = s:len() 103 | 104 | --we search for the head and tail of the string iteratively 105 | --we could fuse these loops, but two separate loops is a lot easier to follow 106 | --and branches less as well. 107 | local head = 0 108 | for i = 1, len do 109 | if not _whitespace_bytes[s:byte(i)] then 110 | head = i 111 | break 112 | end 113 | end 114 | 115 | local tail = 0 116 | for i = len, 1, -1 do 117 | if not _whitespace_bytes[s:byte(i)] then 118 | tail = i 119 | break 120 | end 121 | end 122 | 123 | --overlapping ranges means no content 124 | if head > tail then 125 | return "" 126 | end 127 | --limit ranges means no trim 128 | if head == 1 and tail == len then 129 | return s 130 | end 131 | 132 | --pull out the content 133 | return s:sub(head, tail) 134 | end 135 | 136 | --trim the start of a string 137 | function stringx.ltrim(s) 138 | local head = 1 139 | for i = 1, #s do 140 | if not _whitespace_bytes[s:byte(i)] then 141 | head = i 142 | break 143 | end 144 | end 145 | if head == 1 then 146 | return s 147 | end 148 | return s:sub(head) 149 | end 150 | 151 | --trim the end of a string 152 | function stringx.rtrim(s) 153 | local tail = #s 154 | 155 | for i = #s, 1, -1 do 156 | if not _whitespace_bytes[s:byte(i)] then 157 | tail = i 158 | break 159 | end 160 | end 161 | 162 | if tail == #s then 163 | return s 164 | end 165 | 166 | return s:sub(1, tail) 167 | end 168 | 169 | function stringx.deindent(s, keep_trailing_empty) 170 | --detect windows or unix newlines 171 | local windows_newlines = s:find("\r\n", nil, true) 172 | local newline = windows_newlines and "\r\n" or "\n" 173 | --split along newlines 174 | local lines = stringx.split(s, newline) 175 | --detect and strip any leading blank lines 176 | while lines[1] == "" do 177 | table.remove(lines, 1) 178 | end 179 | 180 | --nothing to do 181 | if #lines == 0 then 182 | return "" 183 | end 184 | 185 | --detect indent 186 | local _, _, indent = lines[1]:find("^([ \t]*)") 187 | local indent_len = indent and indent:len() or 0 188 | 189 | --not indented 190 | if indent_len == 0 then 191 | return table.concat(lines, newline) 192 | end 193 | 194 | --de-indent the lines 195 | local res = {} 196 | for _, line in ipairs(lines) do 197 | if line ~= "" then 198 | local line_start = line:sub(1, indent:len()) 199 | local start_len = line_start:len() 200 | if 201 | line_start == indent 202 | or ( 203 | start_len < indent_len 204 | and line_start == indent:sub(1, start_len) 205 | ) 206 | then 207 | line = line:sub(start_len + 1) 208 | end 209 | end 210 | table.insert(res, line) 211 | end 212 | 213 | --should we keep any trailing empty lines? 214 | if not keep_trailing_empty then 215 | while res[#res] == "" do 216 | table.remove(res) 217 | end 218 | end 219 | 220 | return table.concat(res, newline) 221 | end 222 | 223 | --alias 224 | stringx.dedent = stringx.deindent 225 | 226 | --apply a template to a string 227 | --supports $template style values, given as a table or function 228 | -- ie ("hello $name"):format({name = "tom"}) == "hello tom" 229 | function stringx.apply_template(s, sub) 230 | local r = s:gsub("%$([%w_]+)", sub) 231 | return r 232 | end 233 | 234 | --check if a given string contains another 235 | --(without garbage) 236 | function stringx.contains(haystack, needle) 237 | for i = 1, #haystack - #needle + 1 do 238 | local found = true 239 | for j = 1, #needle do 240 | if haystack:byte(i + j - 1) ~= needle:byte(j) then 241 | found = false 242 | break 243 | end 244 | end 245 | if found then 246 | return true 247 | end 248 | end 249 | return false 250 | end 251 | 252 | --check if a given string starts with another 253 | --(without garbage) 254 | --Using loops is actually faster than string.find! 255 | function stringx.starts_with(s, prefix) 256 | for i = 1, #prefix do 257 | if s:byte(i) ~= prefix:byte(i) then 258 | return false 259 | end 260 | end 261 | return true 262 | end 263 | 264 | --check if a given string ends with another 265 | --(without garbage) 266 | function stringx.ends_with(s, suffix) 267 | local len = #s 268 | local suffix_len = #suffix 269 | for i = 0, suffix_len - 1 do 270 | if s:byte(len - i) ~= suffix:byte(suffix_len - i) then 271 | return false 272 | end 273 | end 274 | return true 275 | end 276 | 277 | --split elements by delimiter and trim the results, discarding empties 278 | --useful for hand-entered "permissive" data 279 | -- "a,b, c, " -> {"a", "b", "c"} 280 | function stringx.split_and_trim(s, delim) 281 | s = stringx.split(s, delim) 282 | for i = #s, 1, -1 do 283 | local v = stringx.trim(s[i]) 284 | if v == "" then 285 | table.remove(s, i) 286 | else 287 | s[i] = v 288 | end 289 | end 290 | return s 291 | end 292 | 293 | --titlizes a string 294 | --"quick brown fox" becomes "Quick Brown Fox" 295 | function stringx.title_case(s) 296 | s = s:gsub("%s%l", string.upper) 297 | s = s:gsub("^%l", string.upper) 298 | 299 | return s 300 | end 301 | 302 | return stringx 303 | -------------------------------------------------------------------------------- /tablex.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | extra table routines 3 | ]] 4 | 5 | local path = (...):gsub("tablex", "") 6 | local assert = require(path .. "assert") 7 | 8 | --for spairs 9 | --(can be replaced with eg table.sort to use that instead) 10 | local sort = require(path .. "sort") 11 | local spairs_sort = sort.stable_sort 12 | 13 | --apply prototype to module if it isn't the global table 14 | --so it works "as if" it was the global table api 15 | --upgraded with these routines 16 | local tablex = setmetatable({}, { 17 | __index = table, 18 | }) 19 | 20 | --alias 21 | tablex.join = tablex.concat 22 | 23 | --return the front element of a table 24 | function tablex.front(t) 25 | return t[1] 26 | end 27 | 28 | --return the back element of a table 29 | function tablex.back(t) 30 | return t[#t] 31 | end 32 | 33 | --remove the back element of a table and return it 34 | function tablex.pop(t) 35 | return table.remove(t) 36 | end 37 | 38 | --insert to the back of a table, returning the table for possible chaining 39 | function tablex.push(t, v) 40 | table.insert(t, v) 41 | return t 42 | end 43 | 44 | --remove the front element of a table and return it 45 | function tablex.shift(t) 46 | return table.remove(t, 1) 47 | end 48 | 49 | --insert to the front of a table, returning the table for possible chaining 50 | function tablex.unshift(t, v) 51 | table.insert(t, 1, v) 52 | return t 53 | end 54 | 55 | --swap two indices of a table 56 | --(easier to read and generally less typing than the common idiom) 57 | function tablex.swap(t, i, j) 58 | t[i], t[j] = t[j], t[i] 59 | end 60 | 61 | --swap the element at i to the back of the table, and remove it 62 | --avoids linear cost of removal at the expense of messing with the order of the table 63 | function tablex.swap_and_pop(t, i) 64 | tablex.swap(t, i, #t) 65 | return tablex.pop(t) 66 | end 67 | 68 | --rotate the elements of a table t by amount slots 69 | -- amount 1: {1, 2, 3, 4} -> {2, 3, 4, 1} 70 | -- amount -1: {1, 2, 3, 4} -> {4, 1, 2, 3} 71 | function tablex.rotate(t, amount) 72 | if #t > 1 then 73 | while amount >= 1 do 74 | tablex.push(t, tablex.shift(t)) 75 | amount = amount - 1 76 | end 77 | while amount <= -1 do 78 | tablex.unshift(t, tablex.pop(t)) 79 | amount = amount + 1 80 | end 81 | end 82 | return t 83 | end 84 | 85 | --default comparison from sort.lua 86 | local default_less = sort.default_less 87 | 88 | --check if a function is sorted based on a "less" or "comes before" ordering comparison 89 | --if any item is "less" than the item before it, we are not sorted 90 | --(use stable_sort to ) 91 | function tablex.is_sorted(t, less) 92 | less = less or default_less 93 | for i = 1, #t - 1 do 94 | if less(t[i + 1], t[i]) then 95 | return false 96 | end 97 | end 98 | return true 99 | end 100 | 101 | --insert to the first position before the first larger element in the table 102 | -- ({1, 2, 2, 3}, 2) -> {1, 2, 2, 2 (inserted here), 3} 103 | --if this is used on an already sorted table, the table will remain sorted and not need re-sorting 104 | --(you can sort beforehand if you don't know) 105 | --return the table for possible chaining 106 | function tablex.insert_sorted(t, v, less) 107 | less = less or default_less 108 | local low = 1 109 | local high = #t 110 | while low <= high do 111 | local mid = math.floor((low + high) / 2) 112 | local mid_val = t[mid] 113 | if less(v, mid_val) then 114 | high = mid - 1 115 | else 116 | low = mid + 1 117 | end 118 | end 119 | table.insert(t, low, v) 120 | return t 121 | end 122 | 123 | --find the index in a sequential table that a resides at 124 | --or nil if nothing was found 125 | function tablex.index_of(t, a) 126 | if a == nil then return nil end 127 | for i, b in ipairs(t) do 128 | if a == b then 129 | return i 130 | end 131 | end 132 | return nil 133 | end 134 | 135 | --find the key in a keyed table that a resides at 136 | --or nil if nothing was found 137 | function tablex.key_of(t, a) 138 | if a == nil then return nil end 139 | for k, v in pairs(t) do 140 | if a == v then 141 | return k 142 | end 143 | end 144 | return nil 145 | end 146 | 147 | --remove the first instance of value from a table (linear search) 148 | --returns true if the value was removed, else false 149 | function tablex.remove_value(t, a) 150 | local i = tablex.index_of(t, a) 151 | if i then 152 | table.remove(t, i) 153 | return true 154 | end 155 | return false 156 | end 157 | 158 | --add a value to a table if it doesn't already exist (linear search) 159 | --returns true if the value was added, else false 160 | function tablex.add_value(t, a) 161 | local i = tablex.index_of(t, a) 162 | if not i then 163 | table.insert(t, a) 164 | return true 165 | end 166 | return false 167 | end 168 | 169 | --get the next element in a sequential table 170 | -- wraps around such that the next element to the last in sequence is the first 171 | -- exists because builtin next may not behave as expected for mixed array/hash tables 172 | -- if the element passed is not present or is nil, will also get the first element 173 | -- but this should not be used to iterate the whole table; just use ipairs for that 174 | function tablex.next_element(t, v) 175 | local i = tablex.index_of(t, v) 176 | --not present? just get the front of the table 177 | if not i then 178 | return tablex.front(t) 179 | end 180 | --(not using mathx to avoid inter-dependency) 181 | i = (i % #t) + 1 182 | return t[i] 183 | end 184 | 185 | function tablex.previous_element(t, v) 186 | local i = tablex.index_of(t, v) 187 | --not present? just get the front of the table 188 | if not i then 189 | return tablex.front(t) 190 | end 191 | --(not using mathx to avoid inter-dependency) 192 | i = i - 1 193 | if i == 0 then 194 | i = #t 195 | end 196 | return t[i] 197 | end 198 | 199 | --note: keyed versions of the above aren't required; you can't double 200 | --up values under keys 201 | 202 | --helper for optionally passed random; defaults to love.math.random if present, otherwise math.random 203 | local _global_random = math.random 204 | if love and love.math and love.math.random then 205 | _global_random = love.math.random 206 | end 207 | local function _random(min, max, r) 208 | return r and r:random(min, max) 209 | or _global_random(min, max) 210 | end 211 | 212 | --pick a random value from a table (or nil if it's empty) 213 | function tablex.random_index(t, r) 214 | if #t == 0 then 215 | return 0 216 | end 217 | return _random(1, #t, r) 218 | end 219 | 220 | --pick a random value from a table (or nil if it's empty) 221 | function tablex.pick_random(t, r) 222 | if #t == 0 then 223 | return nil 224 | end 225 | return t[tablex.random_index(t, r)] 226 | end 227 | 228 | --take a random value from a table (or nil if it's empty) 229 | function tablex.take_random(t, r) 230 | if #t == 0 then 231 | return nil 232 | end 233 | return table.remove(t, tablex.random_index(t, r)) 234 | end 235 | 236 | --return a random value from table t based on weights w provided (or nil empty) 237 | -- w should be the same length as t 238 | -- todo: 239 | -- provide normalisation outside of this function, require normalised weights 240 | function tablex.pick_weighted_random(t, w, r) 241 | if #t == 0 then 242 | return nil 243 | end 244 | if #w ~= #t then 245 | error("tablex.pick_weighted_random weight and value tables should be the same length") 246 | end 247 | local sum = 0 248 | for _, weight in ipairs(w) do 249 | sum = sum + weight 250 | end 251 | local rnd = _random(nil, nil, r) * sum 252 | sum = 0 253 | for i, weight in ipairs(w) do 254 | sum = sum + weight 255 | if rnd <= sum then 256 | return t[i] 257 | end 258 | end 259 | --shouldn't get here but safety if using a random that returns >= 1 260 | return tablex.back(t) 261 | end 262 | 263 | --shuffle the order of a table 264 | function tablex.shuffle(t, r) 265 | for i = 1, #t do 266 | local j = _random(i, #t, r) 267 | t[i], t[j] = t[j], t[i] 268 | end 269 | return t 270 | end 271 | 272 | --reverse the order of a table 273 | function tablex.reverse(t) 274 | for i = 1, #t / 2 do 275 | local j = #t - i + 1 276 | t[i], t[j] = t[j], t[i] 277 | end 278 | return t 279 | end 280 | 281 | --trim a table to a certain maximum length 282 | function tablex.trim(t, l) 283 | while #t > l do 284 | table.remove(t) 285 | end 286 | return t 287 | end 288 | 289 | --collect all keys of a table into a sequential table 290 | --(useful if you need to iterate non-changing keys often and want an nyi tradeoff; 291 | -- this call will be slow but then following iterations can use ipairs) 292 | function tablex.keys(t) 293 | local r = {} 294 | for k, v in pairs(t) do 295 | table.insert(r, k) 296 | end 297 | return r 298 | end 299 | 300 | --collect all values of a keyed table into a sequential table 301 | --(shallow copy if it's already sequential) 302 | function tablex.values(t) 303 | local r = {} 304 | for k, v in pairs(t) do 305 | table.insert(r, v) 306 | end 307 | return r 308 | end 309 | 310 | --collect all values over a range into a new sequential table 311 | --useful where a range may have been modified to contain nils 312 | -- range can be a number, where it is used as a numeric limit (ie [1-range]) 313 | -- range can be a table, where the sequential values are used as keys 314 | function tablex.compact(t, range) 315 | local r = {} 316 | if type(range) == "table" then 317 | for _, k in ipairs(range) do 318 | local v = t[k] 319 | if v then 320 | table.insert(r, v) 321 | end 322 | end 323 | elseif type(range) == "number" then 324 | for i = 1, range do 325 | local v = t[i] 326 | if v then 327 | table.insert(r, v) 328 | end 329 | end 330 | else 331 | error("tablex.compact - range must be a number or table", 2) 332 | end 333 | return r 334 | 335 | end 336 | 337 | --append sequence t2 into t1, modifying t1 338 | function tablex.append_inplace(t1, t2, ...) 339 | for i, v in ipairs(t2) do 340 | table.insert(t1, v) 341 | end 342 | if ... then 343 | return tablex.append_inplace(t1, ...) 344 | end 345 | return t1 346 | end 347 | 348 | --return a new sequence with the elements of both t1 and t2 349 | function tablex.append(t1, ...) 350 | local r = {} 351 | tablex.append_inplace(r, t1, ...) 352 | return r 353 | end 354 | 355 | --return a copy of a sequence with all duplicates removed 356 | -- causes a little "extra" gc churn of one table to track the duplicates internally 357 | function tablex.dedupe(t) 358 | local seen = {} 359 | local r = {} 360 | for i, v in ipairs(t) do 361 | if not seen[v] then 362 | seen[v] = true 363 | table.insert(r, v) 364 | end 365 | end 366 | return r 367 | end 368 | 369 | --(might already exist depending on environment) 370 | if not tablex.clear then 371 | local imported 372 | --pull in from luajit if possible 373 | imported, tablex.clear = pcall(require, "table.clear") 374 | if not imported then 375 | --remove all values from a table 376 | --useful when multiple references are being held 377 | --so you cannot just create a new table 378 | function tablex.clear(t) 379 | assert:type(t, "table", "tablex.clear - t", 1) 380 | local k = next(t) 381 | while k ~= nil do 382 | t[k] = nil 383 | k = next(t) 384 | end 385 | end 386 | end 387 | end 388 | 389 | -- Copy a table 390 | -- See shallow_overlay to shallow copy into an existing table to avoid garbage. 391 | function tablex.shallow_copy(t) 392 | assert:type(t, "table", "tablex.shallow_copy - t", 1) 393 | local into = {} 394 | for k, v in pairs(t) do 395 | into[k] = v 396 | end 397 | return into 398 | end 399 | 400 | --alias 401 | tablex.copy = tablex.shallow_copy 402 | 403 | --implementation for deep copy 404 | --traces stuff that has already been copied, to handle circular references 405 | local function _deep_copy_impl(t, already_copied) 406 | local clone = t 407 | if type(t) == "table" then 408 | if already_copied[t] then 409 | --something we've already encountered before 410 | clone = already_copied[t] 411 | elseif type(t.copy) == "function" then 412 | --something that provides its own copy function 413 | clone = t:copy() 414 | assert:type(clone, "table", "member copy() function didn't return a copy") 415 | else 416 | --a plain table to clone 417 | clone = {} 418 | already_copied[t] = clone 419 | for k, v in pairs(t) do 420 | clone[k] = _deep_copy_impl(v, already_copied) 421 | end 422 | setmetatable(clone, getmetatable(t)) 423 | end 424 | end 425 | return clone 426 | end 427 | 428 | -- Recursively copy values of a table. 429 | -- Retains the same keys as original table -- they're not cloned. 430 | function tablex.deep_copy(t) 431 | assert:type(t, "table", "tablex.deep_copy - t", 1) 432 | return _deep_copy_impl(t, {}) 433 | end 434 | 435 | -- Overlay tables directly onto one another, merging them together. 436 | -- Doesn't merge tables within. 437 | -- Takes as many tables as required, 438 | -- overlays them in passed order onto the first, 439 | -- and returns the first table. 440 | function tablex.shallow_overlay(dest, ...) 441 | assert:type(dest, "table", "tablex.shallow_overlay - dest", 1) 442 | for i = 1, select("#", ...) do 443 | local t = select(i, ...) 444 | assert:type(t, "table", "tablex.shallow_overlay - ...", 1) 445 | for k, v in pairs(t) do 446 | dest[k] = v 447 | end 448 | end 449 | return dest 450 | end 451 | 452 | tablex.overlay = tablex.shallow_overlay 453 | 454 | -- Overlay tables directly onto one another, merging them together into something like a union. 455 | -- Also overlays nested tables, but doesn't clone them (so a nested table may be added to dest). 456 | -- Takes as many tables as required, 457 | -- overlays them in passed order onto the first, 458 | -- and returns the first table. 459 | function tablex.deep_overlay(dest, ...) 460 | assert:type(dest, "table", "tablex.deep_overlay - dest", 1) 461 | for i = 1, select("#", ...) do 462 | local t = select(i, ...) 463 | assert:type(t, "table", "tablex.deep_overlay - ...", 1) 464 | for k, v in pairs(t) do 465 | if type(v) == "table" and type(dest[k]) == "table" then 466 | tablex.deep_overlay(dest[k], v) 467 | else 468 | dest[k] = v 469 | end 470 | end 471 | end 472 | return dest 473 | end 474 | 475 | --collapse the first level of a table into a new table of reduced dimensionality 476 | --will collapse {{1, 2}, 3, {4, 5, 6}} into {1, 2, 3, 4, 5, 6} 477 | --useful when collating multiple result sets, or when you got 2d data when you wanted 1d data. 478 | --in the former case you may just want to append_inplace though :) 479 | --note that non-tabular elements in the base level are preserved, 480 | -- but _all_ tables are collapsed; this includes any table-based types (eg a batteries.vec2), 481 | -- so they can't exist in the base level 482 | -- (... or at least, their non-ipairs members won't survive the collapse) 483 | function tablex.collapse(t) 484 | assert:type(t, "table", "tablex.collapse - t", 1) 485 | local r = {} 486 | for _, v in ipairs(t) do 487 | if type(v) == "table" then 488 | for _, w in ipairs(v) do 489 | table.insert(r, w) 490 | end 491 | else 492 | table.insert(r, v) 493 | end 494 | end 495 | return r 496 | end 497 | 498 | --extract values of a table into nested tables of a set length 499 | -- extract({1, 2, 3, 4}, 2) -> {{1, 2}, {3, 4}} 500 | -- useful for working with "inlined" data in a more structured way 501 | -- can use collapse (or functional.stitch) to reverse the process once you're done if needed 502 | -- todo: support an ordered list of keys passed and extract them to names 503 | function tablex.extract(t, n) 504 | assert:type(t, "table", "tablex.extract - t", 1) 505 | assert:type(n, "number", "tablex.extract - n", 1) 506 | local r = {} 507 | for i = 1, #t, n do 508 | r[i] = {} 509 | for j = 1, n do 510 | table.insert(r[i], t[i + j]) 511 | end 512 | end 513 | return r 514 | end 515 | 516 | --check if two tables have equal contents at the first level 517 | --slow, as it needs two loops 518 | function tablex.shallow_equal(a, b) 519 | if a == b then return true end 520 | for k, v in pairs(a) do 521 | if b[k] ~= v then 522 | return false 523 | end 524 | end 525 | -- second loop to ensure a isn't missing any keys from b. 526 | -- we don't compare the values - if any are missing we're not equal 527 | for k, v in pairs(b) do 528 | if a[k] == nil then 529 | return false 530 | end 531 | end 532 | return true 533 | end 534 | 535 | --check if two tables have equal contents all the way down 536 | --slow, as it needs two potentially recursive loops 537 | function tablex.deep_equal(a, b) 538 | if a == b then return true end 539 | --not equal on type 540 | if type(a) ~= type(b) then 541 | return false 542 | end 543 | --bottomed out 544 | if type(a) ~= "table" then 545 | return a == b 546 | end 547 | for k, v in pairs(a) do 548 | if not tablex.deep_equal(v, b[k]) then 549 | return false 550 | end 551 | end 552 | -- second loop to ensure a isn't missing any keys from b 553 | -- we don't compare the values - if any are missing we're not equal 554 | for k, v in pairs(b) do 555 | if a[k] == nil then 556 | return false 557 | end 558 | end 559 | return true 560 | end 561 | 562 | --alias 563 | tablex.flatten = tablex.collapse 564 | 565 | --faster unpacking for known-length tables up to 8 566 | --gets around nyi in luajit 567 | --note: you can use a larger unpack than you need as the rest 568 | -- can be discarded, but it "feels dirty" :) 569 | 570 | function tablex.unpack2(t) 571 | return t[1], t[2] 572 | end 573 | 574 | function tablex.unpack3(t) 575 | return t[1], t[2], t[3] 576 | end 577 | 578 | function tablex.unpack4(t) 579 | return t[1], t[2], t[3], t[4] 580 | end 581 | 582 | function tablex.unpack5(t) 583 | return t[1], t[2], t[3], t[4], t[5] 584 | end 585 | 586 | function tablex.unpack6(t) 587 | return t[1], t[2], t[3], t[4], t[5], t[6] 588 | end 589 | 590 | function tablex.unpack7(t) 591 | return t[1], t[2], t[3], t[4], t[5], t[6], t[7] 592 | end 593 | 594 | function tablex.unpack8(t) 595 | return t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8] 596 | end 597 | 598 | --internal: reverse iterator function 599 | local function _ripairs_iter(t, i) 600 | i = i - 1 601 | local v = t[i] 602 | if v then 603 | return i, v 604 | end 605 | end 606 | 607 | --iterator that works like ipairs, but in reverse order, with indices from #t to 1 608 | --similar to ipairs, it will only consider sequential until the first nil value in the table. 609 | function tablex.ripairs(t) 610 | return _ripairs_iter, t, #t + 1 611 | end 612 | 613 | --works like pairs, but returns sorted table 614 | -- generates a fair bit of garbage but very nice for more stable output 615 | -- less function gets keys the of the table as its argument; if you want to sort on the values they map to then 616 | -- you'll likely need a closure 617 | function tablex.spairs(t, less) 618 | less = less or default_less 619 | --gather the keys 620 | local keys = tablex.keys(t) 621 | 622 | spairs_sort(keys, less) 623 | 624 | local i = 0 625 | return function() 626 | i = i + 1 627 | if keys[i] then 628 | return keys[i], t[keys[i]] 629 | end 630 | end 631 | end 632 | 633 | 634 | return tablex 635 | -------------------------------------------------------------------------------- /timer.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | basic timer class 3 | 4 | can check for expiry and register a callback to be called on progress and on finish 5 | 6 | if you find yourself using lots of these for pushing stuff into the future, 7 | look into async.lua and see if it might be a better fit! 8 | ]] 9 | 10 | local path = (...):gsub("timer", "") 11 | local class = require(path .. "class") 12 | local timer = class({ 13 | name = "timer", 14 | }) 15 | 16 | --create a timer, with optional callbacks 17 | --callbacks receive as arguments: 18 | -- the current progress as a number from 0 to 1, so can be used for lerps 19 | -- the timer object, so can be reset if needed 20 | function timer:new(time, on_progress, on_finish) 21 | self.time = 0 22 | self.timer = 0 23 | self.on_progress = on_progress 24 | self.on_finish = on_finish 25 | self:reset(time) 26 | end 27 | 28 | --update this timer, calling the relevant callback if it exists 29 | function timer:update(dt) 30 | if not self:expired() then 31 | self.timer = self.timer + dt 32 | 33 | --set the expired state and get the relevant callback 34 | self.has_expired = self.timer >= self.time 35 | local cb = self:expired() 36 | and self.on_finish 37 | or self.on_progress 38 | 39 | if cb then 40 | cb(self:progress(), self) 41 | end 42 | end 43 | end 44 | 45 | --check if the timer has expired 46 | function timer:expired() 47 | return self.has_expired 48 | end 49 | 50 | --get the timer's progress from 0 to 1 51 | function timer:progress() 52 | return math.min(self.timer / self.time, 1) 53 | end 54 | 55 | --reset the timer; optionally change the time 56 | --will resume calling the same callbacks, so can be used for intervals 57 | function timer:reset(time) 58 | self.timer = 0 59 | self.time = math.max(time or self.time, 1e-6) --negative time not allowed 60 | self.has_expired = false 61 | return self 62 | end 63 | 64 | return timer 65 | -------------------------------------------------------------------------------- /vec2.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 2d vector type 3 | ]] 4 | 5 | local path = (...):gsub("vec2", "") 6 | local class = require(path .. "class") 7 | local math = require(path .. "mathx") --shadow global math module 8 | local make_pooled = require(path .. "make_pooled") 9 | 10 | local vec2 = class({ 11 | name = "vec2", 12 | }) 13 | 14 | --stringification 15 | function vec2:__tostring() 16 | return ("(%.2f, %.2f)"):format(self.x, self.y) 17 | end 18 | 19 | --ctor 20 | function vec2:new(x, y) 21 | if type(x) == "number" then 22 | self:scalar_set(x, y) 23 | elseif type(x) == "table" then 24 | if type(x.type) == "function" and x:type() == "vec2" then 25 | self:vector_set(x) 26 | elseif x[1] then 27 | self:scalar_set(x[1], x[2]) 28 | else 29 | self:scalar_set(x.x, x.y) 30 | end 31 | else 32 | self:scalar_set(0) 33 | end 34 | end 35 | 36 | --explicit ctors; mostly vestigial at this point 37 | function vec2:copy() 38 | return vec2(self.x, self.y) 39 | end 40 | 41 | function vec2:xy(x, y) 42 | return vec2(x, y) 43 | end 44 | 45 | function vec2:polar(length, angle) 46 | return vec2(length, 0):rotate_inplace(angle) 47 | end 48 | 49 | function vec2:filled(v) 50 | return vec2(v, v) 51 | end 52 | 53 | function vec2:zero() 54 | return vec2(0) 55 | end 56 | 57 | --unpack for multi-args 58 | function vec2:unpack() 59 | return self.x, self.y 60 | end 61 | 62 | --pack when a sequence is needed 63 | function vec2:pack() 64 | return {self:unpack()} 65 | end 66 | 67 | --shared pooled storage 68 | make_pooled(vec2, 128) 69 | 70 | --get a pooled copy of an existing vector 71 | function vec2:pooled_copy() 72 | return vec2:pooled(self) 73 | end 74 | 75 | --modify 76 | 77 | function vec2:vector_set(v) 78 | self.x = v.x 79 | self.y = v.y 80 | return self 81 | end 82 | 83 | function vec2:scalar_set(x, y) 84 | if not y then y = x end 85 | self.x = x 86 | self.y = y 87 | return self 88 | end 89 | 90 | function vec2:swap(v) 91 | local sx, sy = self.x, self.y 92 | self.x, self.y = v.x, v.y 93 | v.x, v.y = sx, sy 94 | return self 95 | end 96 | 97 | ----------------------------------------------------------- 98 | --equality comparison 99 | ----------------------------------------------------------- 100 | 101 | --threshold for equality in each dimension 102 | local EQUALS_EPSILON = 1e-9 103 | 104 | --true if a and b are functionally equivalent 105 | function vec2.equals(a, b) 106 | return ( 107 | math.abs(a.x - b.x) <= EQUALS_EPSILON and 108 | math.abs(a.y - b.y) <= EQUALS_EPSILON 109 | ) 110 | end 111 | 112 | --true if a and b are not functionally equivalent 113 | --(very slightly faster than `not vec2.equals(a, b)`) 114 | function vec2.nequals(a, b) 115 | return ( 116 | math.abs(a.x - b.x) > EQUALS_EPSILON or 117 | math.abs(a.y - b.y) > EQUALS_EPSILON 118 | ) 119 | end 120 | 121 | --alias 122 | vec2.not_equals = vec2.nequals 123 | 124 | ----------------------------------------------------------- 125 | --arithmetic 126 | ----------------------------------------------------------- 127 | 128 | --vector 129 | function vec2:vector_add_inplace(v) 130 | self.x = self.x + v.x 131 | self.y = self.y + v.y 132 | return self 133 | end 134 | 135 | function vec2:vector_sub_inplace(v) 136 | self.x = self.x - v.x 137 | self.y = self.y - v.y 138 | return self 139 | end 140 | 141 | function vec2:vector_mul_inplace(v) 142 | self.x = self.x * v.x 143 | self.y = self.y * v.y 144 | return self 145 | end 146 | 147 | function vec2:vector_div_inplace(v) 148 | self.x = self.x / v.x 149 | self.y = self.y / v.y 150 | return self 151 | end 152 | 153 | --(a + (b * t)) 154 | --useful for integrating physics and adding directional offsets 155 | function vec2:fused_multiply_add_inplace(v, t) 156 | self.x = self.x + (v.x * t) 157 | self.y = self.y + (v.y * t) 158 | return self 159 | end 160 | 161 | --scalar 162 | function vec2:scalar_add_inplace(x, y) 163 | if not y then y = x end 164 | self.x = self.x + x 165 | self.y = self.y + y 166 | return self 167 | end 168 | 169 | function vec2:scalar_sub_inplace(x, y) 170 | if not y then y = x end 171 | self.x = self.x - x 172 | self.y = self.y - y 173 | return self 174 | end 175 | 176 | function vec2:scalar_mul_inplace(x, y) 177 | if not y then y = x end 178 | self.x = self.x * x 179 | self.y = self.y * y 180 | return self 181 | end 182 | 183 | function vec2:scalar_div_inplace(x, y) 184 | if not y then y = x end 185 | self.x = self.x / x 186 | self.y = self.y / y 187 | return self 188 | end 189 | 190 | ----------------------------------------------------------- 191 | -- geometric methods 192 | ----------------------------------------------------------- 193 | 194 | function vec2:length_squared() 195 | return self.x * self.x + self.y * self.y 196 | end 197 | 198 | function vec2:length() 199 | return math.sqrt(self:length_squared()) 200 | end 201 | 202 | function vec2:distance_squared(other) 203 | local dx = self.x - other.x 204 | local dy = self.y - other.y 205 | return dx * dx + dy * dy 206 | end 207 | 208 | function vec2:distance(other) 209 | return math.sqrt(self:distance_squared(other)) 210 | end 211 | 212 | function vec2:normalise_both_inplace() 213 | local len = self:length() 214 | if len == 0 then 215 | return self, 0 216 | end 217 | return self:scalar_div_inplace(len), len 218 | end 219 | 220 | function vec2:normalise_inplace() 221 | local v, len = self:normalise_both_inplace() 222 | return v 223 | end 224 | 225 | function vec2:normalise_len_inplace() 226 | local v, len = self:normalise_both_inplace() 227 | return len 228 | end 229 | 230 | function vec2:inverse_inplace() 231 | return self:scalar_mul_inplace(-1) 232 | end 233 | 234 | -- angle/direction specific 235 | 236 | function vec2:rotate_inplace(angle) 237 | local s = math.sin(angle) 238 | local c = math.cos(angle) 239 | local ox = self.x 240 | local oy = self.y 241 | self.x = c * ox - s * oy 242 | self.y = s * ox + c * oy 243 | return self 244 | end 245 | 246 | function vec2:rotate_around_inplace(angle, pivot) 247 | self:vector_sub_inplace(pivot) 248 | self:rotate_inplace(angle) 249 | self:vector_add_inplace(pivot) 250 | return self 251 | end 252 | 253 | --fast quarter/half rotations 254 | function vec2:rot90r_inplace() 255 | local ox = self.x 256 | local oy = self.y 257 | self.x = -oy 258 | self.y = ox 259 | return self 260 | end 261 | 262 | function vec2:rot90l_inplace() 263 | local ox = self.x 264 | local oy = self.y 265 | self.x = oy 266 | self.y = -ox 267 | return self 268 | end 269 | 270 | vec2.rot180_inplace = vec2.inverse_inplace --alias 271 | 272 | --get the angle of this vector relative to (1, 0) 273 | function vec2:angle() 274 | return math.atan2(self.y, self.x) 275 | end 276 | 277 | --get the normalised difference in angle between two vectors 278 | function vec2:angle_difference(v) 279 | return math.angle_difference(self:angle(), v:angle()) 280 | end 281 | 282 | --lerp towards the direction of a provided vector 283 | --(length unchanged) 284 | function vec2:lerp_direction_inplace(v, t) 285 | return self:rotate_inplace(self:angle_difference(v) * t) 286 | end 287 | 288 | ----------------------------------------------------------- 289 | -- per-component clamping ops 290 | ----------------------------------------------------------- 291 | 292 | function vec2:min_inplace(v) 293 | self.x = math.min(self.x, v.x) 294 | self.y = math.min(self.y, v.y) 295 | return self 296 | end 297 | 298 | function vec2:max_inplace(v) 299 | self.x = math.max(self.x, v.x) 300 | self.y = math.max(self.y, v.y) 301 | return self 302 | end 303 | 304 | function vec2:clamp_inplace(min, max) 305 | self.x = math.clamp(self.x, min.x, max.x) 306 | self.y = math.clamp(self.y, min.y, max.y) 307 | return self 308 | end 309 | 310 | ----------------------------------------------------------- 311 | -- absolute value 312 | ----------------------------------------------------------- 313 | 314 | function vec2:abs_inplace() 315 | self.x = math.abs(self.x) 316 | self.y = math.abs(self.y) 317 | return self 318 | end 319 | 320 | ----------------------------------------------------------- 321 | -- sign 322 | ----------------------------------------------------------- 323 | 324 | function vec2:sign_inplace() 325 | self.x = math.sign(self.x) 326 | self.y = math.sign(self.y) 327 | return self 328 | end 329 | 330 | ----------------------------------------------------------- 331 | -- truncation/rounding 332 | ----------------------------------------------------------- 333 | 334 | function vec2:floor_inplace() 335 | self.x = math.floor(self.x) 336 | self.y = math.floor(self.y) 337 | return self 338 | end 339 | 340 | function vec2:ceil_inplace() 341 | self.x = math.ceil(self.x) 342 | self.y = math.ceil(self.y) 343 | return self 344 | end 345 | 346 | function vec2:round_inplace() 347 | self.x = math.round(self.x) 348 | self.y = math.round(self.y) 349 | return self 350 | end 351 | 352 | ----------------------------------------------------------- 353 | -- interpolation 354 | ----------------------------------------------------------- 355 | 356 | function vec2:lerp_inplace(other, amount) 357 | self.x = math.lerp(self.x, other.x, amount) 358 | self.y = math.lerp(self.y, other.y, amount) 359 | return self 360 | end 361 | 362 | function vec2:lerp_eps_inplace(other, amount, eps) 363 | self.x = math.lerp_eps(self.x, other.x, amount, eps) 364 | self.y = math.lerp_eps(self.y, other.y, amount, eps) 365 | return self 366 | end 367 | 368 | ----------------------------------------------------------- 369 | -- vector products and projections 370 | ----------------------------------------------------------- 371 | 372 | function vec2:dot(other) 373 | return self.x * other.x + self.y * other.y 374 | end 375 | 376 | --"fake", but useful - also called the wedge product apparently 377 | function vec2:cross(other) 378 | return self.x * other.y - self.y * other.x 379 | end 380 | 381 | function vec2:scalar_projection(other) 382 | local len = other:length() 383 | if len == 0 then 384 | return 0 385 | end 386 | return self:dot(other) / len 387 | end 388 | 389 | function vec2:vector_projection_inplace(other) 390 | local div = other:dot(other) 391 | if div == 0 then 392 | return self:scalar_set(0) 393 | end 394 | local fac = self:dot(other) / div 395 | return self:vector_set(other):scalar_mul_inplace(fac) 396 | end 397 | 398 | function vec2:vector_rejection_inplace(other) 399 | local tx, ty = self.x, self.y 400 | self:vector_projection_inplace(other) 401 | self:scalar_set(tx - self.x, ty - self.y) 402 | return self 403 | end 404 | 405 | --get the winding side of p, relative to the line a-b 406 | -- (this is based on the signed area of the triangle a-b-p) 407 | -- return value: 408 | -- >0 when p left of line 409 | -- =0 when p on line 410 | -- <0 when p right of line 411 | function vec2.winding_side(a, b, p) 412 | return (b.x - a.x) * (p.y - a.y) 413 | - (p.x - a.x) * (b.y - a.y) 414 | end 415 | 416 | --return whether a is nearer to v than b 417 | function vec2.nearer(v, a, b) 418 | return v:distance_squared(a) < v:distance_squared(b) 419 | end 420 | 421 | ----------------------------------------------------------- 422 | -- vector extension methods for special purposes 423 | -- (any common vector ops worth naming) 424 | ----------------------------------------------------------- 425 | 426 | --"physical" friction 427 | function vec2:apply_friction_inplace(mu, dt) 428 | local friction = self:pooled_copy():scalar_mul_inplace(mu * dt) 429 | if friction:length_squared() > self:length_squared() then 430 | self:scalar_set(0, 0) 431 | else 432 | self:vector_sub_inplace(friction) 433 | end 434 | friction:release() 435 | return self 436 | end 437 | 438 | --"gamey" friction in one dimension 439 | local function _friction_1d(v, mu, dt) 440 | local friction = mu * v * dt 441 | if math.abs(friction) > math.abs(v) then 442 | return 0 443 | else 444 | return v - friction 445 | end 446 | end 447 | 448 | --"gamey" friction in both dimensions 449 | function vec2:apply_friction_xy_inplace(mu_x, mu_y, dt) 450 | self.x = _friction_1d(self.x, mu_x, dt) 451 | self.y = _friction_1d(self.y, mu_y, dt) 452 | return self 453 | end 454 | 455 | --minimum/maximum components 456 | function vec2:mincomp() 457 | return math.min(self.x, self.y) 458 | end 459 | 460 | function vec2:maxcomp() 461 | return math.max(self.x, self.y) 462 | end 463 | 464 | -- meta functions for mathmatical operations 465 | function vec2.__add(a, b) 466 | return a:vector_add(b) 467 | end 468 | 469 | function vec2.__sub(a, b) 470 | return a:vector_sub(b) 471 | end 472 | 473 | function vec2.__mul(a, b) 474 | if type(a) == "number" then 475 | return b:scalar_mul(a) 476 | elseif type(b) == "number" then 477 | return a:scalar_mul(b) 478 | else 479 | return a:vector_mul(b) 480 | end 481 | end 482 | 483 | function vec2.__div(a, b) 484 | if type(b) == "number" then 485 | return a:scalar_div(b) 486 | else 487 | return a:vector_div(b) 488 | end 489 | end 490 | 491 | -- mask out min component, with preference to keep x 492 | function vec2:major_inplace() 493 | if self.x > self.y then 494 | self.y = 0 495 | else 496 | self.x = 0 497 | end 498 | return self 499 | end 500 | -- mask out max component, with preference to keep x 501 | function vec2:minor_inplace() 502 | if self.x < self.y then 503 | self.y = 0 504 | else 505 | self.x = 0 506 | end 507 | return self 508 | end 509 | 510 | --vector_ free alias; we're a vector library, so semantics should default to vector 511 | vec2.add_inplace = vec2.vector_add_inplace 512 | vec2.sub_inplace = vec2.vector_sub_inplace 513 | vec2.mul_inplace = vec2.vector_mul_inplace 514 | vec2.div_inplace = vec2.vector_div_inplace 515 | vec2.set = vec2.vector_set 516 | 517 | --american spelling alias 518 | vec2.normalize_both_inplace = vec2.normalise_both_inplace 519 | vec2.normalize_inplace = vec2.normalise_inplace 520 | vec2.normalize_len_inplace = vec2.normalise_len_inplace 521 | 522 | --garbage generating functions that return a new vector rather than modifying self 523 | for _, inplace_name in ipairs({ 524 | "vector_add_inplace", 525 | "vector_sub_inplace", 526 | "vector_mul_inplace", 527 | "vector_div_inplace", 528 | "fused_multiply_add_inplace", 529 | "add_inplace", 530 | "sub_inplace", 531 | "mul_inplace", 532 | "div_inplace", 533 | "scalar_add_inplace", 534 | "scalar_sub_inplace", 535 | "scalar_mul_inplace", 536 | "scalar_div_inplace", 537 | "normalise_both_inplace", 538 | "normalise_inplace", 539 | "normalise_len_inplace", 540 | "normalize_both_inplace", 541 | "normalize_inplace", 542 | "normalize_len_inplace", 543 | "inverse_inplace", 544 | "rotate_inplace", 545 | "rotate_around_inplace", 546 | "rot90r_inplace", 547 | "rot90l_inplace", 548 | "lerp_direction_inplace", 549 | "min_inplace", 550 | "max_inplace", 551 | "clamp_inplace", 552 | "abs_inplace", 553 | "sign_inplace", 554 | "floor_inplace", 555 | "ceil_inplace", 556 | "round_inplace", 557 | "lerp_inplace", 558 | "lerp_eps_inplace", 559 | "vector_projection_inplace", 560 | "vector_rejection_inplace", 561 | "apply_friction_inplace", 562 | "apply_friction_xy_inplace", 563 | "major_inplace", 564 | "minor_inplace", 565 | }) do 566 | local garbage_name = inplace_name:gsub("_inplace", "") 567 | vec2[garbage_name] = function(self, ...) 568 | self = self:copy() 569 | return self[inplace_name](self, ...) 570 | end 571 | end 572 | 573 | --"hungarian" shorthand aliases for compatibility and short names 574 | -- 575 | --i do encourage using the longer versions above as it makes code easier 576 | --to understand when you come back, but i also appreciate wanting short code 577 | for _, v in ipairs({ 578 | {"sset", "scalar_set"}, 579 | {"sadd", "scalar_add"}, 580 | {"ssub", "scalar_sub"}, 581 | {"smul", "scalar_mul"}, 582 | {"sdiv", "scalar_div"}, 583 | {"vset", "vector_set"}, 584 | {"vadd", "vector_add"}, 585 | {"vsub", "vector_sub"}, 586 | {"vmul", "vector_mul"}, 587 | {"vdiv", "vector_div"}, 588 | --(no plain addi etc, imo it's worth differentiating vaddi vs saddi) 589 | {"fma", "fused_multiply_add"}, 590 | {"vproj", "vector_projection"}, 591 | {"vrej", "vector_rejection"}, 592 | --just for the _inplace -> i shorthand, mostly for backwards compatibility 593 | {"min", "min"}, 594 | {"max", "max"}, 595 | {"clamp", "clamp"}, 596 | {"abs", "abs"}, 597 | {"sign", "sign"}, 598 | {"floor", "floor"}, 599 | {"ceil", "ceil"}, 600 | {"round", "round"}, 601 | {"lerp", "lerp"}, 602 | {"rotate", "rotate"}, 603 | {"normalise", "normalise"}, 604 | {"normalize", "normalize"}, 605 | }) do 606 | local shorthand, original = v[1], v[2] 607 | if vec2[shorthand] == nil then 608 | vec2[shorthand] = vec2[original] 609 | end 610 | --and inplace version 611 | shorthand = shorthand .. "i" 612 | original = original .. "_inplace" 613 | if vec2[shorthand] == nil then 614 | vec2[shorthand] = vec2[original] 615 | end 616 | end 617 | 618 | return vec2 619 | -------------------------------------------------------------------------------- /vec3.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3d vector type 3 | ]] 4 | 5 | --import vec2 if not defined globally 6 | local path = (...):gsub("vec3", "") 7 | local class = require(path .. "class") 8 | local vec2 = require(path .. "vec2") 9 | local math = require(path .. "mathx") --shadow global math module 10 | local make_pooled = require(path .. "make_pooled") 11 | 12 | local vec3 = class({ 13 | name = "vec3", 14 | }) 15 | 16 | --stringification 17 | function vec3:__tostring() 18 | return ("(%.2f, %.2f, %.2f)"):format(self.x, self.y, self.z) 19 | end 20 | 21 | --probably-too-flexible ctor 22 | function vec3:new(x, y, z) 23 | if type(x) == "number" or type(x) == "nil" then 24 | self:sset(x or 0, y, z) 25 | elseif type(x) == "table" then 26 | if type(x.type) == "function" and x:type() == "vec3" then 27 | self:vset(x) 28 | elseif x[1] then 29 | self:sset(x[1], x[2], x[3]) 30 | else 31 | self:sset(x.x, x.y, x.z) 32 | end 33 | end 34 | end 35 | 36 | --explicit ctors 37 | function vec3:copy() 38 | return vec3(self.x, self.y, self.z) 39 | end 40 | 41 | function vec3:xyz(x, y, z) 42 | return vec3(x, y, z) 43 | end 44 | 45 | function vec3:filled(x, y, z) 46 | return vec3(x, y, z) 47 | end 48 | 49 | function vec3:zero() 50 | return vec3(0, 0, 0) 51 | end 52 | 53 | --unpack for multi-args 54 | function vec3:unpack() 55 | return self.x, self.y, self.z 56 | end 57 | 58 | --pack when a sequence is needed 59 | function vec3:pack() 60 | return {self:unpack()} 61 | end 62 | 63 | --handle pooling 64 | make_pooled(vec3, 128) 65 | 66 | --get a pooled copy of an existing vector 67 | function vec3:pooled_copy() 68 | return vec3:pooled():vset(self) 69 | end 70 | 71 | --modify 72 | 73 | function vec3:sset(x, y, z) 74 | self.x = x 75 | self.y = y or x 76 | self.z = z or y or x 77 | return self 78 | end 79 | 80 | function vec3:vset(v) 81 | self.x = v.x 82 | self.y = v.y 83 | self.z = v.z 84 | return self 85 | end 86 | 87 | function vec3:swap(v) 88 | local sx, sy, sz = self.x, self.y, self.z 89 | self:vset(v) 90 | v:sset(sx, sy, sz) 91 | return self 92 | end 93 | 94 | ----------------------------------------------------------- 95 | --equality comparison 96 | ----------------------------------------------------------- 97 | 98 | --threshold for equality in each dimension 99 | local EQUALS_EPSILON = 1e-9 100 | 101 | --true if a and b are functionally equivalent 102 | function vec3.equals(a, b) 103 | return ( 104 | math.abs(a.x - b.x) <= EQUALS_EPSILON and 105 | math.abs(a.y - b.y) <= EQUALS_EPSILON and 106 | math.abs(a.z - b.z) <= EQUALS_EPSILON 107 | ) 108 | end 109 | 110 | --true if a and b are not functionally equivalent 111 | --(very slightly faster than `not vec3.equals(a, b)`) 112 | function vec3.nequals(a, b) 113 | return ( 114 | math.abs(a.x - b.x) > EQUALS_EPSILON or 115 | math.abs(a.y - b.y) > EQUALS_EPSILON or 116 | math.abs(a.z - b.z) > EQUALS_EPSILON 117 | ) 118 | end 119 | 120 | ----------------------------------------------------------- 121 | --arithmetic 122 | ----------------------------------------------------------- 123 | 124 | --immediate mode 125 | 126 | --vector 127 | function vec3:vaddi(v) 128 | self.x = self.x + v.x 129 | self.y = self.y + v.y 130 | self.z = self.z + v.z 131 | return self 132 | end 133 | 134 | function vec3:vsubi(v) 135 | self.x = self.x - v.x 136 | self.y = self.y - v.y 137 | self.z = self.z - v.z 138 | return self 139 | end 140 | 141 | function vec3:vmuli(v) 142 | self.x = self.x * v.x 143 | self.y = self.y * v.y 144 | self.z = self.z * v.z 145 | return self 146 | end 147 | 148 | function vec3:vdivi(v) 149 | self.x = self.x / v.x 150 | self.y = self.y / v.y 151 | self.z = self.z / v.z 152 | return self 153 | end 154 | 155 | --scalar 156 | function vec3:saddi(x, y, z) 157 | if not y then y = x end 158 | if not z then z = y end 159 | self.x = self.x + x 160 | self.y = self.y + y 161 | self.z = self.z + z 162 | return self 163 | end 164 | 165 | function vec3:ssubi(x, y, z) 166 | if not y then y = x end 167 | if not z then z = y end 168 | self.x = self.x - x 169 | self.y = self.y - y 170 | self.z = self.z - z 171 | return self 172 | end 173 | 174 | function vec3:smuli(x, y, z) 175 | if not y then y = x end 176 | if not z then z = y end 177 | self.x = self.x * x 178 | self.y = self.y * y 179 | self.z = self.z * z 180 | return self 181 | end 182 | 183 | function vec3:sdivi(x, y, z) 184 | if not y then y = x end 185 | if not z then z = y end 186 | self.x = self.x / x 187 | self.y = self.y / y 188 | self.z = self.z / z 189 | return self 190 | end 191 | 192 | --garbage mode 193 | 194 | function vec3:vadd(v) 195 | return self:copy():vaddi(v) 196 | end 197 | 198 | function vec3:vsub(v) 199 | return self:copy():vsubi(v) 200 | end 201 | 202 | function vec3:vmul(v) 203 | return self:copy():vmuli(v) 204 | end 205 | 206 | function vec3:vdiv(v) 207 | return self:copy():vdivi(v) 208 | end 209 | 210 | function vec3:sadd(x, y, z) 211 | return self:copy():saddi(x, y, z) 212 | end 213 | 214 | function vec3:ssub(x, y, z) 215 | return self:copy():ssubi(x, y, z) 216 | end 217 | 218 | function vec3:smul(x, y, z) 219 | return self:copy():smuli(x, y, z) 220 | end 221 | 222 | function vec3:sdiv(x, y, z) 223 | return self:copy():sdivi(x, y, z) 224 | end 225 | 226 | --fused multiply-add (a + (b * t)) 227 | 228 | function vec3:fmai(v, t) 229 | self.x = self.x + (v.x * t) 230 | self.y = self.y + (v.y * t) 231 | self.z = self.z + (v.z * t) 232 | return self 233 | end 234 | 235 | function vec3:fma(v, t) 236 | return self:copy():fmai(v, t) 237 | end 238 | 239 | ----------------------------------------------------------- 240 | -- geometric methods 241 | ----------------------------------------------------------- 242 | 243 | function vec3:length_squared() 244 | return self.x * self.x + self.y * self.y + self.z * self.z 245 | end 246 | 247 | function vec3:length() 248 | return math.sqrt(self:length_squared()) 249 | end 250 | 251 | function vec3:distance_squared(other) 252 | local dx = self.x - other.x 253 | local dy = self.y - other.y 254 | local dz = self.z - other.z 255 | return dx * dx + dy * dy + dz * dz 256 | end 257 | 258 | function vec3:distance(other) 259 | return math.sqrt(self:distance_squared(other)) 260 | end 261 | 262 | --immediate mode 263 | 264 | function vec3:normalisei_both() 265 | local len = self:length() 266 | if len == 0 then 267 | return self, 0 268 | end 269 | return self:sdivi(len), len 270 | end 271 | 272 | function vec3:normalisei() 273 | local v, len = self:normalisei_both() 274 | return v 275 | end 276 | 277 | function vec3:normalisei_len() 278 | local v, len = self:normalisei_both() 279 | return len 280 | end 281 | 282 | function vec3:inversei() 283 | return self:smuli(-1) 284 | end 285 | 286 | --swizzle extraction 287 | --not as nice as property accessors so might be worth doing that later :) 288 | 289 | --also dog slow, so there's that 290 | local _swizzle_x_byte = ("x"):byte() 291 | local _swizzle_y_byte = ("y"):byte() 292 | local _swizzle_z_byte = ("z"):byte() 293 | local _allowed_swizzle = { 294 | [_swizzle_x_byte] = "x", 295 | [_swizzle_y_byte] = "y", 296 | [_swizzle_z_byte] = "z", 297 | } 298 | 299 | function vec3:encode_swizzle_field(swizzle) 300 | if type(swizzle) == "string" then 301 | swizzle = swizzle:byte() 302 | end 303 | return _allowed_swizzle[swizzle] or "x" 304 | end 305 | 306 | function vec3:extract_single(swizzle) 307 | return self[self:encode_swizzle_field(swizzle)] 308 | end 309 | 310 | function vec3:infuse_single(swizzle, v) 311 | self[self:encode_swizzle_field(swizzle)] = v 312 | return self 313 | end 314 | 315 | function vec3:extract_vec2(swizzle, into) 316 | if not into then into = vec2:zero() end 317 | local x = self:extract_single(swizzle:byte(1)) 318 | local y = self:extract_single(swizzle:byte(2)) 319 | return into:sset(x, y) 320 | end 321 | 322 | function vec3:infuse_vec2(swizzle, v) 323 | self:infuse_single(swizzle:byte(1), v.x) 324 | self:infuse_single(swizzle:byte(2), v.y) 325 | return self 326 | end 327 | 328 | --rotate around a swizzle 329 | --todo: angle-axis version 330 | function vec3:rotatei(swizzle, angle) 331 | if angle == 0 then --early out 332 | return self 333 | end 334 | local v = vec2:pooled() 335 | self:extract_vec2(swizzle, v) 336 | v:rotatei(angle) 337 | self:infuse_vec2(swizzle, v) 338 | v:release() 339 | return self 340 | end 341 | 342 | function vec3:rotate_euleri(angle_x_axis, angle_y_axis, angle_z_axis) 343 | self:rotatei("yz", angle_x_axis) 344 | self:rotatei("xz", angle_y_axis) 345 | self:rotatei("xy", angle_z_axis) 346 | return self 347 | end 348 | 349 | --todo: 90deg rotations 350 | 351 | vec3.rot180i = vec3.inversei --alias 352 | 353 | function vec3:rotate_aroundi(swizzle, angle, pivot) 354 | self:vsubi(pivot) 355 | self:rotatei(swizzle, angle) 356 | self:vaddi(pivot) 357 | return self 358 | end 359 | 360 | --garbage mode 361 | 362 | function vec3:normalised() 363 | return self:copy():normalisei() 364 | end 365 | 366 | function vec3:normalised_len() 367 | local v = self:copy() 368 | local len = v:normalisei_len() 369 | return v, len 370 | end 371 | 372 | function vec3:inverse() 373 | return self:copy():inversei() 374 | end 375 | 376 | function vec3:rotate(swizzle, angle) 377 | return self:copy():rotatei(swizzle, angle) 378 | end 379 | 380 | function vec3:rotate_euler(angle_x_axis, angle_y_axis, angle_z_axis) 381 | return self:copy():rotate_euleri(angle_x_axis, angle_y_axis, angle_z_axis) 382 | end 383 | 384 | vec3.rot180 = vec3.inverse --alias 385 | 386 | function vec3:rotate_around(swizzle, angle, pivot) 387 | return self:copy():rotate_aroundi(swizzle, angle, pivot) 388 | end 389 | 390 | 391 | ----------------------------------------------------------- 392 | -- per-component clamping ops 393 | ----------------------------------------------------------- 394 | 395 | function vec3:mini(v) 396 | self.x = math.min(self.x, v.x) 397 | self.y = math.min(self.y, v.y) 398 | self.z = math.min(self.z, v.z) 399 | return self 400 | end 401 | 402 | function vec3:maxi(v) 403 | self.x = math.max(self.x, v.x) 404 | self.y = math.max(self.y, v.y) 405 | self.z = math.max(self.z, v.z) 406 | return self 407 | end 408 | 409 | function vec3:clampi(min, max) 410 | self.x = math.clamp(self.x, min.x, max.x) 411 | self.y = math.clamp(self.y, min.y, max.y) 412 | self.z = math.clamp(self.z, min.z, max.z) 413 | return self 414 | end 415 | 416 | function vec3:min(v) 417 | return self:copy():mini(v) 418 | end 419 | 420 | function vec3:max(v) 421 | return self:copy():maxi(v) 422 | end 423 | 424 | function vec3:clamp(min, max) 425 | return self:copy():clampi(min, max) 426 | end 427 | 428 | ----------------------------------------------------------- 429 | -- absolute value 430 | ----------------------------------------------------------- 431 | 432 | function vec3:absi() 433 | self.x = math.abs(self.x) 434 | self.y = math.abs(self.y) 435 | self.z = math.abs(self.z) 436 | return self 437 | end 438 | 439 | function vec3:abs() 440 | return self:copy():absi() 441 | end 442 | 443 | ----------------------------------------------------------- 444 | -- truncation/rounding 445 | ----------------------------------------------------------- 446 | 447 | function vec3:floori() 448 | self.x = math.floor(self.x) 449 | self.y = math.floor(self.y) 450 | self.z = math.floor(self.z) 451 | return self 452 | end 453 | 454 | function vec3:ceili() 455 | self.x = math.ceil(self.x) 456 | self.y = math.ceil(self.y) 457 | self.z = math.ceil(self.z) 458 | return self 459 | end 460 | 461 | function vec3:roundi() 462 | self.x = math.round(self.x) 463 | self.y = math.round(self.y) 464 | self.z = math.round(self.z) 465 | return self 466 | end 467 | 468 | function vec3:floor() 469 | return self:copy():floori() 470 | end 471 | 472 | function vec3:ceil() 473 | return self:copy():ceili() 474 | end 475 | 476 | function vec3:round() 477 | return self:copy():roundi() 478 | end 479 | 480 | ----------------------------------------------------------- 481 | -- interpolation 482 | ----------------------------------------------------------- 483 | 484 | function vec3:lerpi(other, amount) 485 | self.x = math.lerp(self.x, other.x, amount) 486 | self.y = math.lerp(self.y, other.y, amount) 487 | self.z = math.lerp(self.z, other.z, amount) 488 | return self 489 | end 490 | 491 | function vec3:lerp(other, amount) 492 | return self:copy():lerpi(other, amount) 493 | end 494 | 495 | function vec3:lerp_epsi(other, amount, eps) 496 | self.x = math.lerp_eps(self.x, other.x, amount, eps) 497 | self.y = math.lerp_eps(self.y, other.y, amount, eps) 498 | self.z = math.lerp_eps(self.z, other.z, amount, eps) 499 | return self 500 | end 501 | 502 | function vec3:lerp_eps(other, amount, eps) 503 | return self:copy():lerp_epsi(other, amount, eps) 504 | end 505 | 506 | ----------------------------------------------------------- 507 | -- vector products and projections 508 | ----------------------------------------------------------- 509 | 510 | function vec3.dot(a, b) 511 | return a.x * b.x + a.y * b.y + a.z * b.z 512 | end 513 | 514 | function vec3.cross(a, b, into) 515 | if not into then into = vec3:zero() end 516 | return into:sset( 517 | a.y * b.z - a.z * b.y, 518 | a.z * b.x - a.x * b.z, 519 | a.x * b.y - a.y * b.x 520 | ) 521 | end 522 | 523 | --scalar projection a onto b 524 | function vec3.sproj(a, b) 525 | local len = b:length() 526 | if len == 0 then 527 | return 0 528 | end 529 | return a:dot(b) / len 530 | end 531 | 532 | --vector projection a onto b (writes into a) 533 | function vec3.vproji(a, b) 534 | local div = b:dot(b) 535 | if div == 0 then 536 | return a:sset(0, 0, 0) 537 | end 538 | local fac = a:dot(b) / div 539 | return a:vset(b):smuli(fac) 540 | end 541 | 542 | function vec3.vproj(a, b) 543 | return a:copy():vproji(b) 544 | end 545 | 546 | --vector rejection a onto b (writes into a) 547 | function vec3.vreji(a, b) 548 | local tx, ty, tz = a.x, a.y, a.z 549 | a:vproji(b) 550 | a:sset(tx - a.x, ty - a.y, tz - a.z) 551 | return a 552 | end 553 | 554 | function vec3.vrej(a, b) 555 | return a:copy():vreji(b) 556 | end 557 | 558 | ----------------------------------------------------------- 559 | -- vector extension methods for special purposes 560 | -- (any common vector ops worth naming) 561 | ----------------------------------------------------------- 562 | 563 | --"physical" friction 564 | local _v_friction = vec3:zero() --avoid alloc 565 | function vec3:apply_friction(mu, dt) 566 | _v_friction:vset(self):smuli(mu * dt) 567 | if _v_friction:length_squared() > self:length_squared() then 568 | self:sset(0, 0) 569 | else 570 | self:vsubi(_v_friction) 571 | end 572 | return self 573 | end 574 | 575 | --"gamey" friction in one dimension 576 | local function apply_friction_1d(v, mu, dt) 577 | local friction = mu * v * dt 578 | if math.abs(friction) > math.abs(v) then 579 | return 0 580 | else 581 | return v - friction 582 | end 583 | end 584 | 585 | --"gamey" friction in both dimensions 586 | function vec3:apply_friction_xy(mu_x, mu_y, dt) 587 | self.x = apply_friction_1d(self.x, mu_x, dt) 588 | self.y = apply_friction_1d(self.y, mu_y, dt) 589 | self.z = apply_friction_1d(self.z, mu_y, dt) 590 | return self 591 | end 592 | 593 | return vec3 594 | --------------------------------------------------------------------------------