├── LICENSE.txt ├── README.md ├── Welcome.link ├── assets ├── CrimsonPro-Light.ttf ├── CrimsonPro-Medium.ttf ├── CrimsonPro-Regular.ttf ├── Lora-VariableFont_wght.ttf ├── SourceCodePro-Regular.ttf ├── SourceSansPro-Regular.ttf ├── akciom-4x9-fixed.png ├── akciom-4x9.png └── akciom-font-variable.png ├── conf.lua ├── data ├── Hitbox.lua ├── OrderedSet.lua ├── PriorityQueue.lua ├── Vector.lua ├── bump.lua ├── debug │ └── Profile.lua ├── ini.lua ├── json.lua ├── keys2.lua ├── main2.lua ├── sap.lua ├── strict.lua ├── sysconsole.lua ├── sysinput.lua └── utils │ ├── README.txt │ ├── args.lua │ ├── color.lua │ ├── io.lua │ ├── logging.lua │ ├── math │ ├── round.lua │ ├── stats.lua │ └── truncate.lua │ ├── printf.lua │ └── string │ ├── check_utf8.lua │ ├── diff_match_patch.lua │ ├── hex_dump.lua │ └── trim.lua ├── dist ├── LOOP License.txt ├── LÖVE License.txt ├── Vector.lua License.txt ├── bump.lua License.txt └── json License.txt ├── game.cfg ├── link.agpack ├── assets │ └── data │ │ ├── default.link │ │ └── icon.png ├── keybind.cfg ├── main.lua └── src │ ├── Animation.lua │ ├── Component.lua │ ├── Directories.lua │ ├── Entity.lua │ └── exif.lua └── main.lua /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Scott Smith 2 | 3 | Except where otherwise noted, all source code and any modifications are 4 | released under the following license (MIT License): 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | 24 | -------- 25 | 26 | Except where otherwise noted, all assets located in the assets directory are 27 | licensed under the Creative Commons Attribution 4.0 International License 28 | (CC BY 4.0). 29 | 30 | -------------------------------------------------------------------------------- 31 | 32 | Additional licensening information can be found in the dist directory. 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LINK! 2 | ===== 3 | 4 | LINK was originally designed to be a free form notetaking application 5 | and to that end, it does an alright job. 6 | 7 | The code is a mess. I hacked on it like there was no tomorrow and it 8 | shows. 9 | 10 | I make extensive use of Vim's marker code folding to organize the code 11 | so if it looks a little out of hand, try that. It should clear things 12 | up at least a little. 13 | 14 | I wish I could spend more time on it but life has other plans for me. I 15 | don't know if I'll ever get a chance to revisit this program so I'm 16 | releasing it as open source to give it one more chance. 17 | 18 | It's been a wild ride. 19 | 20 | Have fun and enjoy the adventure! 21 | -------------------------------------------------------------------------------- /assets/CrimsonPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/CrimsonPro-Light.ttf -------------------------------------------------------------------------------- /assets/CrimsonPro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/CrimsonPro-Medium.ttf -------------------------------------------------------------------------------- /assets/CrimsonPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/CrimsonPro-Regular.ttf -------------------------------------------------------------------------------- /assets/Lora-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/Lora-VariableFont_wght.ttf -------------------------------------------------------------------------------- /assets/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /assets/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /assets/akciom-4x9-fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/akciom-4x9-fixed.png -------------------------------------------------------------------------------- /assets/akciom-4x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/akciom-4x9.png -------------------------------------------------------------------------------- /assets/akciom-font-variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/assets/akciom-font-variable.png -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | DEBUG = true 2 | local loveversion = "11.3" 3 | local sfmt = string.format 4 | 5 | local version, gamename, savedir do 6 | gamename = "LINK!" 7 | savedir = "LINK" 8 | local tagline = "Larry's Ingenious Note Keeper" 9 | local engine = "Emk2.3" --version.gamenum 10 | local preextra = "- INDEV - " 11 | local gamever = "0.8" 12 | local extra = " Alpha (Open Source Edition)" 13 | 14 | do--{{{, Print fullname to console 15 | local fullname = sfmt( 16 | "%s\n%s\n(%s_%s %sversion %s)%s\n", 17 | gamename, tagline, engine, loveversion, preextra, gamever, extra 18 | ) 19 | local line = 1 20 | local s, e, match = 0, 0 21 | local fullnamelen = #fullname 22 | repeat 23 | s, e, match = fullname:find("[^\n]+\n", e+1) 24 | if not e then break end 25 | local matchlen = e-s-1 --remove trailing newline 26 | 27 | local termwidth = 78 28 | local len = math.floor(tonumber((termwidth - matchlen) *.5)) 29 | if len < 0 then len = 0 end 30 | local pre, post, sep = "", "", " " 31 | if line == 1 then 32 | pre, post, sep = "==[[", "]]==", "_" 33 | elseif line == 2 then 34 | pre, post, sep = " \\__", "__/ ", "" 35 | elseif line == 3 then 36 | pre, post, sep = " ", " ", " " 37 | end 38 | if #sep < 1 then sep = " " 39 | elseif #sep > 1 then sep = sep:sub(1, 1) end 40 | 41 | local termwidth = termwidth-#pre-#post 42 | local lwdiff = termwidth-matchlen 43 | if lwdiff > 0 then 44 | lwdiff = 0 45 | end 46 | match = fullname:sub(s, e-1+lwdiff) 47 | if sep ~= " " then 48 | match = match:gsub(" ", sep) 49 | end 50 | io.write(sfmt(" %s%s%s%s%s\n", 51 | pre, sep:rep(len-#pre), match, sep:rep(len-#post-1), post)) 52 | line = line + 1 53 | until #fullname == e + 1 54 | io.write("\n") 55 | end--}}} 56 | 57 | version = sfmt("%s %s%s%s", gamename, preextra, gamever, extra) 58 | end 59 | 60 | hal_conf = { 61 | midi = false, 62 | websocket_enabled = false, 63 | version = version, 64 | savedir = savedir, 65 | reload_main2_on_restart = true, 66 | AG = {}, 67 | } 68 | 69 | function love.conf(t) 70 | t.identity = "Akciom" 71 | --t.appendidentity = false --search files in source dir before save dir 72 | t.version = loveversion 73 | --t.console = false 74 | --t.accelerometerjoystick = true 75 | --t.externalstorage = false 76 | --t.gammacorrect = false 77 | 78 | --t.audio.mic = false 79 | --t.audio.mixwithsystem = true 80 | 81 | t.window = false --[[ 82 | t.window.title = version 83 | --t.window.icon = nil 84 | t.window.width = 854 85 | t.window.height = 480 86 | --t.window.borderless = false 87 | t.window.resizable = true 88 | --t.window.minwidth = 1 89 | --t.window.minheight = 1 90 | --t.window.fullscreen = false 91 | --t.window.fullscreentype = "desktop" --"desktop" or "exclusive" 92 | t.window.vsync = 0 93 | --t.window.msaa = 0 94 | --t.window.depth = nil 95 | --t.window.stencil = nil 96 | t.window.display = 2 97 | --t.window.highdpi = false 98 | --t.window.usedpiscale = true 99 | --t.window.x = nil 100 | --t.window.y = nil 101 | --]] 102 | 103 | --t.modules.audio = true 104 | --t.modules.data = true 105 | --t.modules.event = true 106 | --t.modules.font = true 107 | --t.modules.graphics = true 108 | --t.modules.image = true 109 | --t.modules.joystick = true 110 | --t.modules.keyboard = true 111 | --t.modules.math = true 112 | --t.modules.mouse = true 113 | t.modules.physics = false 114 | --t.modules.sound = true 115 | --t.modules.system = true 116 | --t.modules.thread = true 117 | --t.modules.timer = true 118 | --t.modules.touch = true 119 | --t.modules.video = true 120 | --t.modules.window = true 121 | end 122 | -------------------------------------------------------------------------------- /data/Hitbox.lua: -------------------------------------------------------------------------------- 1 | local type = type 2 | 3 | local ffi = require "ffi" 4 | local ffinew = ffi.new 5 | 6 | ffi.cdef[[ 7 | struct hitbox { 8 | float offx, offy; 9 | float hw, hh; 10 | }; 11 | ]] 12 | 13 | local struct_hitbox_ctype = ffi.typeof("struct hitbox") 14 | 15 | local hitbox = { 16 | _datatype = "hitbox", 17 | } 18 | 19 | local meta = {} 20 | 21 | meta.__index = hitbox 22 | 23 | local strfmt = string.format 24 | function meta:__tostring() 25 | return strfmt("%.2f,%.2f+%.2f,%.2f", 26 | self.offx, self.offy, self.hw, self.hh) 27 | end 28 | 29 | local function istype(v) 30 | return type(v) == "cdata" and v._datatype == "hitbox" 31 | end 32 | 33 | --Can be inialized with (hitbox) or (hw, hh) or (offx, offy, hw, hh) 34 | local function new(arg1, arg2, hw, hh) 35 | local offx = arg1 36 | local offy = arg2 37 | if istype(arg1) then 38 | local hbox = arg1 39 | offx = hbox.offx 40 | offy = hbox.offy 41 | hw = hbox.hw 42 | hh = hbox.hh 43 | elseif hw == nil and hh == nil then 44 | offx, offy = 0, 0 45 | hw = arg1 46 | hh = arg2 47 | end 48 | 49 | local hitbox = ffinew(struct_hitbox_ctype) 50 | hitbox.offx = offx 51 | hitbox.offy = offy 52 | hitbox.hw = hw 53 | hitbox.hh = hh 54 | return hitbox 55 | end 56 | 57 | function hitbox:unpack() 58 | return self.offx, self.offy, self.hw, self.hh 59 | end 60 | 61 | 62 | 63 | ffi.metatype(struct_hitbox_ctype, meta) 64 | 65 | return setmetatable({new = new, istype = istype}, 66 | {__call = function(_, ...) return new(...) end}) 67 | -------------------------------------------------------------------------------- /data/OrderedSet.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | ---------------------- ## ##### ##### ###### ----------------------- 3 | ---------------------- ## ## ## ## ## ## ## ----------------------- 4 | ---------------------- ## ## ## ## ## ###### ----------------------- 5 | ---------------------- ## ## ## ## ## ## ----------------------- 6 | ---------------------- ###### ##### ##### ## ----------------------- 7 | ---------------------- ----------------------- 8 | ----------------------- Lua Object-Oriented Programming ------------------------ 9 | -------------------------------------------------------------------------------- 10 | -- Project: LOOP Class Library -- 11 | -- Release: 2.3 beta -- 12 | -- Title : Ordered Set Optimized for Insertions and Removals -- 13 | -- Author : Renato Maia -- 14 | -- Updated: 0.1.0 by Scott Smith -- 15 | -------------------------------------------------------------------------------- 16 | -- v0.1.0 - Updated to modern Lua -- 17 | -------------------------------------------------------------------------------- 18 | -- Notes: -- 19 | -- Storage of strings equal to the name of one method prevents its usage. -- 20 | -------------------------------------------------------------------------------- 21 | local newproxy = newproxy and newproxy or function () return {} end 22 | local next, type = next, type 23 | local setmetatable = setmetatable 24 | 25 | -------------------------------------------------------------------------------- 26 | -- key constants --------------------------------------------------------------- 27 | -------------------------------------------------------------------------------- 28 | 29 | local FIRST = newproxy() 30 | local LAST = newproxy() 31 | 32 | -------------------------------------------------------------------------------- 33 | -- basic functionality --------------------------------------------------------- 34 | -------------------------------------------------------------------------------- 35 | 36 | local m = {} 37 | 38 | local function iterator(self, previous) 39 | return self[previous], previous 40 | end 41 | 42 | function m.sequence(self) 43 | return iterator, self, FIRST 44 | end 45 | 46 | function m.contains(self, element) 47 | return element ~= nil and (self[element] ~= nil or element == self[LAST]) 48 | end 49 | local contains = m.contains 50 | 51 | function m.first(self) 52 | return self[FIRST] 53 | end 54 | 55 | function m.last(self) 56 | return self[LAST] 57 | end 58 | 59 | function m.isempty(self) 60 | return self[FIRST] == nil 61 | end 62 | 63 | function m.insert(self, element, previous) 64 | if element ~= nil and not contains(self, element) then 65 | if previous == nil then 66 | previous = self[LAST] 67 | if previous == nil then 68 | previous = FIRST 69 | end 70 | elseif not contains(self, previous) and previous ~= FIRST then 71 | return 72 | end 73 | if self[previous] == nil 74 | then self[LAST] = element 75 | else self[element] = self[previous] 76 | end 77 | self[previous] = element 78 | return element 79 | end 80 | end 81 | 82 | function m.previous(self, element, start) 83 | if contains(self, element) then 84 | local previous = (start == nil and FIRST or start) 85 | repeat 86 | if self[previous] == element then 87 | return previous 88 | end 89 | previous = self[previous] 90 | until previous == nil 91 | end 92 | end 93 | 94 | function m.remove(self, element, start) 95 | local prev = previous(self, element, start) 96 | if prev ~= nil then 97 | self[prev] = self[element] 98 | if self[LAST] == element 99 | then self[LAST] = prev 100 | else self[element] = nil 101 | end 102 | return element, prev 103 | end 104 | end 105 | 106 | function m.replace(self, old, new, start) 107 | local prev = previous(self, old, start) 108 | if prev ~= nil and new ~= nil and not contains(self, new) then 109 | self[prev] = new 110 | self[new] = self[old] 111 | if old == self[LAST] 112 | then self[LAST] = new 113 | else self[old] = nil 114 | end 115 | return old, prev 116 | end 117 | end 118 | 119 | function m.pushfront(self, element) 120 | if element ~= nil and not contains(self, element) then 121 | if self[FIRST] ~= nil 122 | then self[element] = self[FIRST] 123 | else self[LAST] = element 124 | end 125 | self[FIRST] = element 126 | return element 127 | end 128 | end 129 | 130 | function m.popfront(self) 131 | local element = self[FIRST] 132 | self[FIRST] = self[element] 133 | if self[FIRST] ~= nil 134 | then self[element] = nil 135 | else self[LAST] = nil 136 | end 137 | return element 138 | end 139 | 140 | function m.pushback(self, element) 141 | if element ~= nil and not contains(self, element) then 142 | if self[LAST] ~= nil 143 | then self[ self[LAST] ] = element 144 | else self[FIRST] = element 145 | end 146 | self[LAST] = element 147 | return element 148 | end 149 | end 150 | 151 | -------------------------------------------------------------------------------- 152 | -- function aliases ------------------------------------------------------------ 153 | -------------------------------------------------------------------------------- 154 | 155 | -- set operations 156 | m.add = m.pushback 157 | 158 | -- stack operations 159 | m.push = m.pushfront 160 | m.pop = m.popfront 161 | m.top = m.first 162 | 163 | -- queue operations 164 | m.enqueue = m.pushback 165 | m.dequeue = m.popfront 166 | m.head = m.first 167 | m.tail = m.last 168 | 169 | --m.firstkey = FIRST 170 | 171 | local function new() 172 | return setmetatable({}, {__index = m}) 173 | end 174 | 175 | --copy functions over to maintain exising compatability 176 | local f = {} 177 | for k,v in next, m do 178 | if type(v) == "function" then 179 | f[k] = v 180 | end 181 | end 182 | f.new = new 183 | 184 | return setmetatable(f, {__call = new}) 185 | -------------------------------------------------------------------------------- /data/PriorityQueue.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | ---------------------- ## ##### ##### ###### ----------------------- 3 | ---------------------- ## ## ## ## ## ## ## ----------------------- 4 | ---------------------- ## ## ## ## ## ###### ----------------------- 5 | ---------------------- ## ## ## ## ## ## ----------------------- 6 | ---------------------- ###### ##### ##### ## ----------------------- 7 | ---------------------- ----------------------- 8 | ----------------------- Lua Object-Oriented Programming ------------------------ 9 | -------------------------------------------------------------------------------- 10 | -- Project: LOOP Class Library -- 11 | -- Release: 2.3 beta -- 12 | -- Title : Priority Queue Optimized for Insertions and Removals -- 13 | -- Author : Renato Maia -- 14 | -- Updated: 0.1.0 by Scott Smith -- 15 | -------------------------------------------------------------------------------- 16 | -- v0.1.0 - Updated to modern Lua -- 17 | -------------------------------------------------------------------------------- 18 | -- Notes: -- 19 | -- Storage of strings equal to the name of one method prevents its usage. -- 20 | -------------------------------------------------------------------------------- 21 | local setmetatable = setmetatable 22 | 23 | local OrderedSet = require "OrderedSet" 24 | 25 | -------------------------------------------------------------------------------- 26 | -- internal constants ---------------------------------------------------------- 27 | -------------------------------------------------------------------------------- 28 | 29 | local PRIORITY = {} 30 | 31 | -------------------------------------------------------------------------------- 32 | -- basic functionality --------------------------------------------------------- 33 | -------------------------------------------------------------------------------- 34 | 35 | local m = {} 36 | 37 | -- internal functions 38 | local function getpriorities(self) 39 | if not self[PRIORITY] then 40 | self[PRIORITY] = {} 41 | end 42 | return self[PRIORITY] 43 | end 44 | local function removepriority(self, element) 45 | if element then 46 | local priorities = getpriorities(self) 47 | local priority = priorities[element] 48 | priorities[element] = nil 49 | return element, priority 50 | end 51 | end 52 | 53 | -- borrowed functions 54 | for k,v in next, OrderedSet do 55 | m[k] = v 56 | end 57 | local sequence = OrderedSet.sequence 58 | local contains = OrderedSet.contains 59 | local isempty = OrderedSet.isempty 60 | local head = OrderedSet.head 61 | local tail = OrderedSet.tail 62 | 63 | -- specific functions 64 | function m.priority(self, element) 65 | return getpriorities(self)[element] 66 | end 67 | 68 | function m.enqueue(self, element, priority) 69 | if not contains(self, element) then 70 | local previous 71 | if priority then 72 | local priorities = getpriorities(self) 73 | for elem, prev in sequence(self) do 74 | local prio = priorities[elem] 75 | if prio and prio > priority then 76 | previous = prev 77 | break 78 | end 79 | end 80 | priorities[element] = priority 81 | end 82 | return OrderedSet.insert(self, element, previous) 83 | end 84 | end 85 | 86 | function m.dequeue(self) 87 | return removepriority(self, OrderedSet.dequeue(self)) 88 | end 89 | 90 | function m.remove(self, element, previous) 91 | return removepriority(self, OrderedSet.remove(self, element, previous)) 92 | end 93 | 94 | local function new() 95 | return setmetatable({}, {__index = m}) 96 | end 97 | 98 | return setmetatable({new = new}, {__call = new}) 99 | -------------------------------------------------------------------------------- /data/Vector.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local assert = assert 28 | local setmetatable, type, tonumber = setmetatable, type, tonumber 29 | local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2 30 | 31 | local ffi = require "ffi" 32 | local ffinew = ffi.new 33 | 34 | local meta = {} 35 | if not hal_defined.vector2 then 36 | hal_defined.vector2 = true 37 | ffi.cdef[[ 38 | struct vector2 { 39 | double x, y; 40 | }; 41 | ]] 42 | ffi.metatype(ffi.typeof("struct vector2") , meta) 43 | end 44 | local struct_vector2_ctype = ffi.typeof("struct vector2") 45 | 46 | local vector = { 47 | _datatype = "vector2", 48 | } 49 | meta.__index = vector 50 | 51 | local function isvector(v) 52 | return type(v) == "cdata" and v._datatype == "vector2" 53 | end 54 | 55 | local function new(vecx, y) 56 | local x = vecx 57 | if isvector(vecx) then 58 | x = vecx.x 59 | y = vecx.y 60 | end 61 | local vec = ffinew(struct_vector2_ctype) 62 | vec.x = x or 0 63 | vec.y = y or 0 64 | return vec 65 | end 66 | 67 | function vector:clone() 68 | return new(self.x, self.y) 69 | end 70 | 71 | function vector:unpack() 72 | return self.x, self.y 73 | end 74 | 75 | local strfmt = string.format 76 | function meta:__tostring() 77 | return strfmt("%.2f,%.2f", self.x, self.y) 78 | end 79 | 80 | function meta.__unm(a) 81 | return new(-a.x, -a.y) 82 | end 83 | 84 | function meta.__add(a,b) 85 | assert(isvector(a) and isvector(b), "Add: wrong argument types ( expected)") 86 | return new(a.x+b.x, a.y+b.y) 87 | end 88 | 89 | function meta.__sub(a,b) 90 | assert(isvector(a) and isvector(b), "Sub: wrong argument types ( expected)") 91 | return new(a.x-b.x, a.y-b.y) 92 | end 93 | 94 | function meta.__mul(a,b) 95 | if type(a) == "number" then 96 | return new(a*b.x, a*b.y) 97 | elseif type(b) == "number" then 98 | return new(b*a.x, b*a.y) 99 | else 100 | assert(isvector(a) and isvector(b), "Mul: wrong argument types ( or expected)") 101 | return a.x*b.x + a.y*b.y 102 | end 103 | end 104 | 105 | function meta.__div(a,b) 106 | assert(isvector(a) and type(b) == "number", "wrong argument types (expected / )") 107 | return new(a.x / b, a.y / b) 108 | end 109 | 110 | function meta.__eq(a,b) 111 | return a.x == b.x and a.y == b.y 112 | end 113 | 114 | function meta.__lt(a,b) 115 | return a.x < b.x or (a.x == b.x and a.y < b.y) 116 | end 117 | 118 | function meta.__le(a,b) 119 | return a.x <= b.x and a.y <= b.y 120 | end 121 | 122 | function vector.permul(a,b) 123 | assert(isvector(a) and isvector(b), "permul: wrong argument types ( expected)") 124 | return new(a.x*b.x, a.y*b.y) 125 | end 126 | 127 | function vector:len2() 128 | return self.x * self.x + self.y * self.y 129 | end 130 | 131 | function vector:len() 132 | return sqrt(self.x * self.x + self.y * self.y) 133 | end 134 | 135 | function vector.dist(a, b) 136 | assert(isvector(a) and isvector(b), "dist: wrong argument types ( expected)") 137 | local dx = a.x - b.x 138 | local dy = a.y - b.y 139 | return sqrt(dx * dx + dy * dy) 140 | end 141 | 142 | function vector.dist2(a, b) 143 | assert(isvector(a) and isvector(b), "dist: wrong argument types ( expected)") 144 | local dx = a.x - b.x 145 | local dy = a.y - b.y 146 | return (dx * dx + dy * dy) 147 | end 148 | 149 | function vector:normalize_inplace(len) 150 | local l = len or self:len() 151 | if l > 0 then 152 | self.x, self.y = self.x / l, self.y / l 153 | end 154 | return self 155 | end 156 | 157 | function vector:normalized(length) 158 | return self:clone():normalize_inplace(length) 159 | end 160 | 161 | function vector:rotate_inplace(phi) 162 | local c, s = cos(phi), sin(phi) 163 | self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y 164 | return self 165 | end 166 | 167 | function vector:rotate_cs_inplace(c, s) 168 | self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y 169 | return self 170 | end 171 | 172 | 173 | function vector:rotated(phi) 174 | local c, s = cos(phi), sin(phi) 175 | return new(c * self.x - s * self.y, s * self.x + c * self.y) 176 | end 177 | 178 | function vector:perpendicular() 179 | return new(-self.y, self.x) 180 | end 181 | 182 | function vector:projectOn(v) 183 | assert(isvector(v), "invalid argument: cannot project vector on " .. type(v)) 184 | -- (self * v) * v / v:len2() 185 | local s = (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y) 186 | return new(s * v.x, s * v.y) 187 | end 188 | 189 | function vector:mirrorOn(v) 190 | assert(isvector(v), "invalid argument: cannot mirror vector on " .. type(v)) 191 | -- 2 * self:projectOn(v) - self 192 | local s = 2 * (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y) 193 | return new(s * v.x - self.x, s * v.y - self.y) 194 | end 195 | 196 | function vector:cross(v) 197 | assert(isvector(v), "cross: wrong argument types ( expected)") 198 | return self.x * v.y - self.y * v.x 199 | end 200 | 201 | -- ref.: http://blog.signalsondisplay.com/?p=336 202 | function vector:trim_inplace(maxLen) 203 | local s = maxLen * maxLen / self:len2() 204 | s = (s > 1 and 1) or sqrt(s) 205 | self.x, self.y = self.x * s, self.y * s 206 | return self 207 | end 208 | 209 | function vector:angleTo(other) 210 | if other then 211 | return atan2(self.y, self.x) - atan2(other.y, other.x) 212 | end 213 | return atan2(self.y, self.x) 214 | end 215 | 216 | function vector:trimmed(maxLen) 217 | return self:clone():trim_inplace(maxLen) 218 | end 219 | 220 | function vector:lerp(nextvec, alpha) 221 | return new( 222 | nextvec.x * alpha + self.x * (1.0-alpha), 223 | nextvec.y * alpha + self.y * (1.0-alpha) 224 | ) 225 | end 226 | 227 | local zero = ffi.new(struct_vector2_ctype) 228 | zero.x, zero.y = 0, 0 229 | 230 | -- the module 231 | return setmetatable({ 232 | new = new, isvector = isvector, istype = isvector, zero = zero 233 | }, 234 | {__call = function(_, ...) return new(...) end}) 235 | -------------------------------------------------------------------------------- /data/bump.lua: -------------------------------------------------------------------------------- 1 | local bump = { 2 | _VERSION = 'bump v3.1.5', 3 | -- _URL = 'https://github.com/kikito/bump.lua', 4 | -- _DESCRIPTION = 'A collision detection library for Lua', 5 | -- _LICENSE = [[ 6 | -- MIT LICENSE 7 | -- 8 | -- Copyright (c) 2014 Enrique García Cota 9 | -- 10 | -- Permission is hereby granted, free of charge, to any person obtaining a 11 | -- copy of this software and associated documentation files (the 12 | -- "Software"), to deal in the Software without restriction, including 13 | -- without limitation the rights to use, copy, modify, merge, publish, 14 | -- distribute, sublicense, and/or sell copies of the Software, and to 15 | -- permit persons to whom the Software is furnished to do so, subject to 16 | -- the following conditions: 17 | -- 18 | -- The above copyright notice and this permission notice shall be included 19 | -- in all copies or substantial portions of the Software. 20 | -- 21 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | -- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | -- ]] 29 | } 30 | 31 | ------------------------------------------ 32 | -- Auxiliary functions 33 | ------------------------------------------ 34 | local DELTA = 1e-10 -- floating-point margin of error 35 | 36 | local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max 37 | 38 | local function sign(x) 39 | if x > 0 then return 1 end 40 | if x == 0 then return 0 end 41 | return -1 42 | end 43 | 44 | local function nearest(x, a, b) 45 | if abs(a - x) < abs(b - x) then return a else return b end 46 | end 47 | 48 | local function assertType(desiredType, value, name) 49 | if type(value) ~= desiredType then 50 | error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')') 51 | end 52 | end 53 | 54 | local function assertIsPositiveNumber(value, name) 55 | if type(value) ~= 'number' or value <= 0 then 56 | error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')') 57 | end 58 | end 59 | 60 | local function assertIsRect(x,y,w,h) 61 | assertType('number', x, 'x') 62 | assertType('number', y, 'y') 63 | assertIsPositiveNumber(w, 'w') 64 | assertIsPositiveNumber(h, 'h') 65 | end 66 | 67 | local defaultFilter = function() 68 | return 'slide' 69 | end 70 | 71 | ------------------------------------------ 72 | -- Rectangle functions 73 | ------------------------------------------ 74 | 75 | local function rect_getNearestCorner(x,y,w,h, px, py) 76 | return nearest(px, x, x+w), nearest(py, y, y+h) 77 | end 78 | 79 | -- This is a generalized implementation of the liang-barsky algorithm, which also returns 80 | -- the normals of the sides where the segment intersects. 81 | -- Returns nil if the segment never touches the rect 82 | -- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge 83 | local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2) 84 | ti1, ti2 = ti1 or 0, ti2 or 1 85 | local dx, dy = x2-x1, y2-y1 86 | local nx, ny 87 | local nx1, ny1, nx2, ny2 = 0,0,0,0 88 | local p, q, r 89 | 90 | for side = 1,4 do 91 | if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left 92 | elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right 93 | elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top 94 | else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom 95 | end 96 | 97 | if p == 0 then 98 | if q <= 0 then return nil end 99 | else 100 | r = q / p 101 | if p < 0 then 102 | if r > ti2 then return nil 103 | elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny 104 | end 105 | else -- p > 0 106 | if r < ti1 then return nil 107 | elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny 108 | end 109 | end 110 | end 111 | end 112 | 113 | return ti1,ti2, nx1,ny1, nx2,ny2 114 | end 115 | 116 | -- Calculates the minkowsky difference between 2 rects, which is another rect 117 | local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) 118 | return x2 - x1 - w1, 119 | y2 - y1 - h1, 120 | w1 + w2, 121 | h1 + h2 122 | end 123 | 124 | local function rect_containsPoint(x,y,w,h, px,py) 125 | return px - x > DELTA and py - y > DELTA and 126 | x + w - px > DELTA and y + h - py > DELTA 127 | end 128 | 129 | local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2) 130 | return x1 < x2+w2 and x2 < x1+w1 and 131 | y1 < y2+h2 and y2 < y1+h1 132 | end 133 | 134 | local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2) 135 | local dx = x1 - x2 + (w1 - w2)/2 136 | local dy = y1 - y2 + (h1 - h2)/2 137 | return dx*dx + dy*dy 138 | end 139 | 140 | local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY) 141 | goalX = goalX or x1 142 | goalY = goalY or y1 143 | 144 | local dx, dy = goalX - x1, goalY - y1 145 | local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) 146 | 147 | local overlaps, ti, nx, ny 148 | 149 | if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other 150 | local px, py = rect_getNearestCorner(x,y,w,h, 0, 0) 151 | local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection 152 | ti = -wi * hi -- ti is the negative area of intersection 153 | overlaps = true 154 | else 155 | local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge) 156 | 157 | -- item tunnels into other 158 | if ti1 and ti1 < 1 and (0 < ti1 + DELTA or 0 == ti1 and ti2 > 0) then 159 | ti, nx, ny = ti1, nx1, ny1 160 | overlaps = false 161 | end 162 | end 163 | 164 | if not ti then return end 165 | 166 | local tx, ty 167 | 168 | if overlaps then 169 | if dx == 0 and dy == 0 then 170 | -- intersecting and not moving - use minimum displacement vector 171 | local px, py = rect_getNearestCorner(x,y,w,h, 0,0) 172 | if abs(px) < abs(py) then py = 0 else px = 0 end 173 | nx, ny = sign(px), sign(py) 174 | tx, ty = x1 + px, y1 + py 175 | else 176 | -- intersecting and moving - move in the opposite direction 177 | local ti1, _ 178 | ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1) 179 | if not ti1 then return end 180 | tx, ty = x1 + dx * ti1, y1 + dy * ti1 181 | end 182 | else -- tunnel 183 | tx, ty = x1 + dx * ti, y1 + dy * ti 184 | end 185 | 186 | return { 187 | overlaps = overlaps, 188 | ti = ti, 189 | move = {x = dx, y = dy}, 190 | normal = {x = nx, y = ny}, 191 | touch = {x = tx, y = ty}, 192 | itemRect = {x = x1, y = y1, w = w1, h = h1}, 193 | otherRect = {x = x2, y = y2, w = w2, h = h2} 194 | } 195 | end 196 | 197 | ------------------------------------------ 198 | -- Grid functions 199 | ------------------------------------------ 200 | 201 | local function grid_toWorld(cellSize, cx, cy) 202 | return (cx - 1)*cellSize, (cy-1)*cellSize 203 | end 204 | 205 | local function grid_toCell(cellSize, x, y) 206 | return floor(x / cellSize) + 1, floor(y / cellSize) + 1 207 | end 208 | 209 | -- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing", 210 | -- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf 211 | -- It has been modified to include both cells when the ray "touches a grid corner", 212 | -- and with a different exit condition 213 | 214 | local function grid_traverse_initStep(cellSize, ct, t1, t2) 215 | local v = t2 - t1 216 | if v > 0 then 217 | return 1, cellSize / v, ((ct + v) * cellSize - t1) / v 218 | elseif v < 0 then 219 | return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v 220 | else 221 | return 0, math.huge, math.huge 222 | end 223 | end 224 | 225 | local function grid_traverse(cellSize, x1,y1,x2,y2, f) 226 | local cx1,cy1 = grid_toCell(cellSize, x1,y1) 227 | local cx2,cy2 = grid_toCell(cellSize, x2,y2) 228 | local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2) 229 | local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2) 230 | local cx,cy = cx1,cy1 231 | 232 | f(cx, cy) 233 | 234 | -- The default implementation had an infinite loop problem when 235 | -- approaching the last cell in some occassions. We finish iterating 236 | -- when we are *next* to the last cell 237 | while abs(cx - cx2) + abs(cy - cy2) > 1 do 238 | if tx < ty then 239 | tx, cx = tx + dx, cx + stepX 240 | f(cx, cy) 241 | else 242 | -- Addition: include both cells when going through corners 243 | if tx == ty then f(cx + stepX, cy) end 244 | ty, cy = ty + dy, cy + stepY 245 | f(cx, cy) 246 | end 247 | end 248 | 249 | -- If we have not arrived to the last cell, use it 250 | if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end 251 | 252 | end 253 | 254 | local function grid_toCellRect(cellSize, x,y,w,h) 255 | local cx,cy = grid_toCell(cellSize, x, y) 256 | local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize) 257 | return cx, cy, cr - cx + 1, cb - cy + 1 258 | end 259 | 260 | ------------------------------------------ 261 | -- Responses 262 | ------------------------------------------ 263 | 264 | local touch = function(world, col, x,y,w,h, goalX, goalY, filter) 265 | return col.touch.x, col.touch.y, {}, 0 266 | end 267 | 268 | local cross = function(world, col, x,y,w,h, goalX, goalY, filter) 269 | local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) 270 | return goalX, goalY, cols, len 271 | end 272 | 273 | local slide = function(world, col, x,y,w,h, goalX, goalY, filter) 274 | goalX = goalX or x 275 | goalY = goalY or y 276 | 277 | local tch, move = col.touch, col.move 278 | local sx, sy = tch.x, tch.y 279 | if move.x ~= 0 or move.y ~= 0 then 280 | if col.normal.x == 0 then 281 | sx = goalX 282 | else 283 | sy = goalY 284 | end 285 | end 286 | 287 | col.slide = {x = sx, y = sy} 288 | 289 | x,y = tch.x, tch.y 290 | goalX, goalY = sx, sy 291 | local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) 292 | return goalX, goalY, cols, len 293 | end 294 | 295 | local bounce = function(world, col, x,y,w,h, goalX, goalY, filter) 296 | goalX = goalX or x 297 | goalY = goalY or y 298 | 299 | local tch, move = col.touch, col.move 300 | local tx, ty = tch.x, tch.y 301 | 302 | local bx, by = tx, ty 303 | 304 | if move.x ~= 0 or move.y ~= 0 then 305 | local bnx, bny = goalX - tx, goalY - ty 306 | if col.normal.x == 0 then bny = -bny else bnx = -bnx end 307 | bx, by = tx + bnx, ty + bny 308 | end 309 | 310 | col.bounce = {x = bx, y = by} 311 | x,y = tch.x, tch.y 312 | goalX, goalY = bx, by 313 | 314 | local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) 315 | return goalX, goalY, cols, len 316 | end 317 | 318 | ------------------------------------------ 319 | -- World 320 | ------------------------------------------ 321 | 322 | local World = {} 323 | local World_mt = {__index = World} 324 | 325 | -- Private functions and methods 326 | 327 | local function sortByWeight(a,b) return a.weight < b.weight end 328 | 329 | local function sortByTiAndDistance(a,b) 330 | if a.ti == b.ti then 331 | local ir, ar, br = a.itemRect, a.otherRect, b.otherRect 332 | local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h) 333 | local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h) 334 | return ad < bd 335 | end 336 | return a.ti < b.ti 337 | end 338 | 339 | local function addItemToCell(self, item, cx, cy) 340 | self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'}) 341 | local row = self.rows[cy] 342 | row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})} 343 | local cell = row[cx] 344 | self.nonEmptyCells[cell] = true 345 | if not cell.items[item] then 346 | cell.items[item] = true 347 | cell.itemCount = cell.itemCount + 1 348 | end 349 | end 350 | 351 | local function removeItemFromCell(self, item, cx, cy) 352 | local row = self.rows[cy] 353 | if not row or not row[cx] or not row[cx].items[item] then return false end 354 | 355 | local cell = row[cx] 356 | cell.items[item] = nil 357 | cell.itemCount = cell.itemCount - 1 358 | if cell.itemCount == 0 then 359 | self.nonEmptyCells[cell] = nil 360 | end 361 | return true 362 | end 363 | 364 | local function getDictItemsInCellRect(self, cl,ct,cw,ch) 365 | local items_dict = {} 366 | for cy=ct,ct+ch-1 do 367 | local row = self.rows[cy] 368 | if row then 369 | for cx=cl,cl+cw-1 do 370 | local cell = row[cx] 371 | if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling 372 | for item,_ in pairs(cell.items) do 373 | items_dict[item] = true 374 | end 375 | end 376 | end 377 | end 378 | end 379 | 380 | return items_dict 381 | end 382 | 383 | local function getCellsTouchedBySegment(self, x1,y1,x2,y2) 384 | 385 | local cells, cellsLen, visited = {}, 0, {} 386 | 387 | grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy) 388 | local row = self.rows[cy] 389 | if not row then return end 390 | local cell = row[cx] 391 | if not cell or visited[cell] then return end 392 | 393 | visited[cell] = true 394 | cellsLen = cellsLen + 1 395 | cells[cellsLen] = cell 396 | end) 397 | 398 | return cells, cellsLen 399 | end 400 | 401 | local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter) 402 | local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2) 403 | local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1 404 | local visited, itemInfo, itemInfoLen = {},{},0 405 | for i=1,len do 406 | cell = cells[i] 407 | for item in pairs(cell.items) do 408 | if not visited[item] then 409 | visited[item] = true 410 | if (not filter or filter(item)) then 411 | rect = self.rects[item] 412 | l,t,w,h = rect.x,rect.y,rect.w,rect.h 413 | 414 | ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1) 415 | if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then 416 | -- the sorting is according to the t of an infinite line, not the segment 417 | tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge) 418 | itemInfoLen = itemInfoLen + 1 419 | itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)} 420 | end 421 | end 422 | end 423 | end 424 | end 425 | table.sort(itemInfo, sortByWeight) 426 | return itemInfo, itemInfoLen 427 | end 428 | 429 | local function getResponseByName(self, name) 430 | local response = self.responses[name] 431 | if not response then 432 | error(sfmt('Unknown collision type: %s (%s)',name, type(name))) 433 | end 434 | return response 435 | end 436 | 437 | 438 | -- Misc Public Methods 439 | 440 | function World:addResponse(name, response) 441 | self.responses[name] = response 442 | end 443 | 444 | function World:project(item, x,y,w,h, goalX, goalY, filter) 445 | assertIsRect(x,y,w,h) 446 | 447 | goalX = goalX or x 448 | goalY = goalY or y 449 | filter = filter or defaultFilter 450 | 451 | local collisions, len = {}, 0 452 | 453 | local visited = {} 454 | if item ~= nil then visited[item] = true end 455 | 456 | -- This could probably be done with less cells using a polygon raster over the cells instead of a 457 | -- bounding rect of the whole movement. Conditional to building a queryPolygon method 458 | local tl, tt = min(goalX, x), min(goalY, y) 459 | local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h) 460 | local tw, th = tr-tl, tb-tt 461 | 462 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th) 463 | 464 | local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) 465 | 466 | for other,_ in pairs(dictItemsInCellRect) do 467 | if not visited[other] then 468 | visited[other] = true 469 | 470 | local responseName = filter(item, other) 471 | if responseName then 472 | local ox,oy,ow,oh = self:getRect(other) 473 | local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY) 474 | 475 | if col then 476 | col.other = other 477 | col.item = item 478 | col.type = responseName 479 | 480 | len = len + 1 481 | collisions[len] = col 482 | end 483 | end 484 | end 485 | end 486 | 487 | table.sort(collisions, sortByTiAndDistance) 488 | 489 | return collisions, len 490 | end 491 | 492 | function World:countCells() 493 | local count = 0 494 | for _,row in pairs(self.rows) do 495 | for _,_ in pairs(row) do 496 | count = count + 1 497 | end 498 | end 499 | return count 500 | end 501 | 502 | function World:hasItem(item) 503 | return not not self.rects[item] 504 | end 505 | 506 | function World:getItems() 507 | local items, len = {}, 0 508 | for item,_ in pairs(self.rects) do 509 | len = len + 1 510 | items[len] = item 511 | end 512 | return items, len 513 | end 514 | 515 | function World:countItems() 516 | local len = 0 517 | for _ in pairs(self.rects) do len = len + 1 end 518 | return len 519 | end 520 | 521 | function World:getRect(item) 522 | local rect = self.rects[item] 523 | if not rect then 524 | error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.') 525 | end 526 | return rect.x, rect.y, rect.w, rect.h 527 | end 528 | 529 | function World:toWorld(cx, cy) 530 | return grid_toWorld(self.cellSize, cx, cy) 531 | end 532 | 533 | function World:toCell(x,y) 534 | return grid_toCell(self.cellSize, x, y) 535 | end 536 | 537 | 538 | --- Query methods 539 | 540 | function World:queryRect(x,y,w,h, filter) 541 | 542 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) 543 | local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) 544 | 545 | local items, len = {}, 0 546 | 547 | local rect 548 | for item,_ in pairs(dictItemsInCellRect) do 549 | rect = self.rects[item] 550 | if (not filter or filter(item)) 551 | and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h) 552 | then 553 | len = len + 1 554 | items[len] = item 555 | end 556 | end 557 | 558 | return items, len 559 | end 560 | 561 | function World:queryPoint(x,y, filter) 562 | local cx,cy = self:toCell(x,y) 563 | local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1) 564 | 565 | local items, len = {}, 0 566 | 567 | local rect 568 | for item,_ in pairs(dictItemsInCellRect) do 569 | rect = self.rects[item] 570 | if (not filter or filter(item)) 571 | and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y) 572 | then 573 | len = len + 1 574 | items[len] = item 575 | end 576 | end 577 | 578 | return items, len 579 | end 580 | 581 | function World:querySegment(x1, y1, x2, y2, filter) 582 | local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) 583 | local items = {} 584 | for i=1, len do 585 | items[i] = itemInfo[i].item 586 | end 587 | return items, len 588 | end 589 | 590 | function World:querySegmentWithCoords(x1, y1, x2, y2, filter) 591 | local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) 592 | local dx, dy = x2-x1, y2-y1 593 | local info, ti1, ti2 594 | for i=1, len do 595 | info = itemInfo[i] 596 | ti1 = info.ti1 597 | ti2 = info.ti2 598 | 599 | info.weight = nil 600 | info.x1 = x1 + dx * ti1 601 | info.y1 = y1 + dy * ti1 602 | info.x2 = x1 + dx * ti2 603 | info.y2 = y1 + dy * ti2 604 | end 605 | return itemInfo, len 606 | end 607 | 608 | 609 | --- Main methods 610 | 611 | function World:add(item, x,y,w,h) 612 | local rect = self.rects[item] 613 | if rect then 614 | error('Item ' .. tostring(item) .. ' added to the world twice.') 615 | end 616 | assertIsRect(x,y,w,h) 617 | 618 | self.rects[item] = {x=x,y=y,w=w,h=h} 619 | 620 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) 621 | for cy = ct, ct+ch-1 do 622 | for cx = cl, cl+cw-1 do 623 | addItemToCell(self, item, cx, cy) 624 | end 625 | end 626 | 627 | return item 628 | end 629 | 630 | function World:remove(item) 631 | local x,y,w,h = self:getRect(item) 632 | 633 | self.rects[item] = nil 634 | local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) 635 | for cy = ct, ct+ch-1 do 636 | for cx = cl, cl+cw-1 do 637 | removeItemFromCell(self, item, cx, cy) 638 | end 639 | end 640 | end 641 | 642 | function World:update(item, x2,y2,w2,h2) 643 | local x1,y1,w1,h1 = self:getRect(item) 644 | w2,h2 = w2 or w1, h2 or h1 645 | assertIsRect(x2,y2,w2,h2) 646 | 647 | if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then 648 | 649 | local cellSize = self.cellSize 650 | local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1) 651 | local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2) 652 | 653 | if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then 654 | 655 | local cr1, cb1 = cl1+cw1-1, ct1+ch1-1 656 | local cr2, cb2 = cl2+cw2-1, ct2+ch2-1 657 | local cyOut 658 | 659 | for cy = ct1, cb1 do 660 | cyOut = cy < ct2 or cy > cb2 661 | for cx = cl1, cr1 do 662 | if cyOut or cx < cl2 or cx > cr2 then 663 | removeItemFromCell(self, item, cx, cy) 664 | end 665 | end 666 | end 667 | 668 | for cy = ct2, cb2 do 669 | cyOut = cy < ct1 or cy > cb1 670 | for cx = cl2, cr2 do 671 | if cyOut or cx < cl1 or cx > cr1 then 672 | addItemToCell(self, item, cx, cy) 673 | end 674 | end 675 | end 676 | 677 | end 678 | 679 | local rect = self.rects[item] 680 | rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2 681 | 682 | end 683 | end 684 | 685 | function World:move(item, goalX, goalY, filter) 686 | local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter) 687 | 688 | self:update(item, actualX, actualY) 689 | 690 | return actualX, actualY, cols, len 691 | end 692 | 693 | function World:check(item, goalX, goalY, filter) 694 | filter = filter or defaultFilter 695 | 696 | local visited = {[item] = true} 697 | local visitedFilter = function(itm, other) 698 | if visited[other] then return false end 699 | return filter(itm, other) 700 | end 701 | 702 | local cols, len = {}, 0 703 | 704 | local x,y,w,h = self:getRect(item) 705 | 706 | local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter) 707 | 708 | while projected_len > 0 do 709 | local col = projected_cols[1] 710 | len = len + 1 711 | cols[len] = col 712 | 713 | visited[col.other] = true 714 | 715 | local response = getResponseByName(self, col.type) 716 | 717 | goalX, goalY, projected_cols, projected_len = response( 718 | self, 719 | col, 720 | x, y, w, h, 721 | goalX, goalY, 722 | visitedFilter 723 | ) 724 | end 725 | 726 | return goalX, goalY, cols, len 727 | end 728 | 729 | 730 | -- Public library functions 731 | 732 | bump.newWorld = function(cellSize) 733 | cellSize = cellSize or 64 734 | assertIsPositiveNumber(cellSize, 'cellSize') 735 | local world = setmetatable({ 736 | cellSize = cellSize, 737 | rects = {}, 738 | rows = {}, 739 | nonEmptyCells = {}, 740 | responses = {} 741 | }, World_mt) 742 | 743 | world:addResponse('touch', touch) 744 | world:addResponse('cross', cross) 745 | world:addResponse('slide', slide) 746 | world:addResponse('bounce', bounce) 747 | 748 | return world 749 | end 750 | 751 | bump.rect = { 752 | getNearestCorner = rect_getNearestCorner, 753 | getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices, 754 | getDiff = rect_getDiff, 755 | containsPoint = rect_containsPoint, 756 | isIntersecting = rect_isIntersecting, 757 | getSquareDistance = rect_getSquareDistance, 758 | detectCollision = rect_detectCollision 759 | } 760 | 761 | bump.responses = { 762 | touch = touch, 763 | cross = cross, 764 | slide = slide, 765 | bounce = bounce 766 | } 767 | 768 | return bump 769 | -------------------------------------------------------------------------------- /data/debug/Profile.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local sfmt = string.format 3 | local floor = math.floor 4 | local table = table 5 | 6 | local f = {} 7 | 8 | local m = {} 9 | 10 | local getTime = love.timer.getTime 11 | 12 | local Hertz 13 | local delta_time 14 | 15 | function m:start() 16 | --self.count = 0 17 | self[0] = getTime() 18 | return true 19 | end 20 | 21 | function m:reset() 22 | self.count = -1 23 | return true 24 | end 25 | 26 | function m:time() 27 | return getTime() - self[0] 28 | end 29 | 30 | function m:lap() 31 | local c = self.count + 1 32 | self.count = c 33 | self[c] = getTime() - self[0] 34 | return self[c] 35 | end 36 | 37 | function m:print(file) 38 | file = file or io.output() 39 | local start = self[0] 40 | local sum = 0 41 | local t = {} 42 | local ti = table.insert 43 | local c = self.count 44 | ti(t, sfmt("Lap Data for \"%s\" (%d laps)", self.name, c)) 45 | for i = 1, c do 46 | local lap = self[i] 47 | --ti(t, sfmt(" Lap %d : %7.4fs", i, lap)) 48 | sum = sum + lap 49 | end 50 | local microsec = floor(sum * 1000000) 51 | local microX = floor((delta_time) * 1000000) 52 | --ti(t, sfmt(" Total : %5d µs", microsec)) 53 | --ti(t, sfmt(" Laps : %5d laps", c)) 54 | local average = microsec / (c < 1 and 1 or c) 55 | self.average = average 56 | self.avgXHz = average / microX * 100 57 | --ti(t, sfmt(" Average : %8.2f µs/lap", average)) 58 | --ti(t, sfmt(" : %.4f%% lap/60Hz", self.avg60Hz)) 59 | 60 | --local data = table.concat(t, "\n") 61 | local data = sfmt("Lap Data %-20s|%7.2f%% of %dHz %8.2f µs/lap %7d laps ", 62 | self.name, self.avgXHz, Hertz, average, c < 0 and 0 or c 63 | ) 64 | 65 | file:write(data.."\n") 66 | return data 67 | end 68 | 69 | function m:dump(file) 70 | self:print(file) 71 | self:reset() 72 | end 73 | 74 | function f:new(name) 75 | local t = { 76 | name = name, 77 | count = -1, 78 | [0] = getTime(), 79 | type = "Profile", 80 | } 81 | 82 | return setmetatable(t, {__index = m}) 83 | end 84 | 85 | local Profile = setmetatable(f, {__call = f.new}) 86 | 87 | local testing = Profile("testing") 88 | --print("testing:start", testing) 89 | --testing:start() 90 | --testing:lap() 91 | --testing:dump() 92 | 93 | 94 | -- UP NEXT : PROFILE MANAGER 95 | 96 | local pmm = {} 97 | local pmf = {} 98 | 99 | local reserved = { 100 | "main", 101 | "update", 102 | "render", 103 | "sleep", 104 | } 105 | for i = 1, #reserved do 106 | reserved[reserved[i]] = true 107 | end 108 | 109 | 110 | --{{{ [[ PROFILE TABLE ]] (m table) 111 | function pmm.new (self, name, desc) 112 | if not self[name] then 113 | --printf("Adding %s\n", name) 114 | local p = Profile(desc) 115 | 116 | local adding = p 117 | if reserved[name] then 118 | local group = { p, count = 1} 119 | adding = group 120 | end 121 | 122 | self[name] = adding 123 | return adding 124 | end 125 | return nil, "name already exists" 126 | end 127 | 128 | local function set_func (self, name, desc) 129 | local p 130 | local tname = type(name) 131 | 132 | if tname == "table" then 133 | p = name 134 | elseif tname == "string" then 135 | local parent = self._parent or self 136 | p = parent[name] 137 | if not p then 138 | p = assert(self:new(name, desc)) 139 | end 140 | if not(type(p) == "table" and p.type == "Profile") then 141 | error("invalid Profile: ".. name) 142 | end 143 | end 144 | --printf("added %s: %s — %s\n", p.type, name, desc) 145 | self.assembled = false 146 | 147 | local c = self.count + 1 148 | self.count = c 149 | 150 | self[c] = p 151 | end 152 | 153 | local function set_index(self, k) 154 | local parent = rawget(self, "_parent") 155 | local g = parent[k] 156 | if g then 157 | g._parent = parent 158 | return function (name, desc) 159 | set_func(g, name, desc) 160 | end 161 | end 162 | end 163 | 164 | local function set_newindex(self, k, v) 165 | if k ~= "_parent" then 166 | error("Cannot set table") 167 | elseif rawget(self, k) then 168 | error("Cannot set parent, already set") 169 | end 170 | rawset(self, k, v) 171 | end 172 | 173 | function pmm.remove (self, name) 174 | local p 175 | local tn = type(name) 176 | if tn == "string" then 177 | p = self[name] 178 | elseif tn == "table" then 179 | p = name 180 | end 181 | local c = self.count 182 | if p then 183 | local rm_i 184 | for i = 1, c do 185 | if self[i] == p then 186 | rm_i = i 187 | break 188 | end 189 | end 190 | if rm_i then 191 | for i = rm_i, c-1 do 192 | self[i] = self[i+1] 193 | end 194 | self.count = c - 1 195 | end 196 | end 197 | end 198 | 199 | function pmm.dump_all (self, dt) 200 | self.timer = self.timer - dt 201 | if self.timer <= 0 then 202 | self.timer = self.timer_default 203 | else 204 | return 205 | end 206 | 207 | if not self.assembed then 208 | local c = 0 209 | for i = 1, #reserved do 210 | local gname = reserved[i] 211 | local grp = self[gname] 212 | if grp then 213 | local len = #grp 214 | --print("Group", gname, "len", len) 215 | for k = 1, len do 216 | --print(" gk", grp[k], "type", grp[k].type) 217 | self[c + k] = grp[k] 218 | end 219 | c = c + len 220 | end 221 | end 222 | self.count = c 223 | self.assembed = true 224 | end 225 | 226 | Hertz = self.Hertz[dt] 227 | if not Hertz then 228 | Hertz = floor(1.0 / dt + 0.5) 229 | self.Hertz[dt] = Hertz 230 | end 231 | delta_time = dt 232 | 233 | printf(" PROFILE LAP DATA @ %.1f s MET\n", hal.met) 234 | for i = 1, self.count do 235 | self[i]:dump() 236 | end 237 | end 238 | --}}} 239 | 240 | function pmf.new(self) 241 | local t = { 242 | timer = 0, 243 | timer = 0, 244 | timer_default = 5, 245 | 246 | count = 0, 247 | assembled = false, 248 | 249 | Hertz = {}, 250 | } 251 | t.set = setmetatable({_parent = t}, { 252 | __call = set_func, 253 | __index = set_index, 254 | __newindex = set_newindex, 255 | }) 256 | 257 | return setmetatable(t, {__call = pmm.new, __index = pmm }) 258 | end 259 | 260 | return pmf.new() 261 | -------------------------------------------------------------------------------- /data/ini.lua: -------------------------------------------------------------------------------- 1 | --[[ Akciom's Flavor of INI Parser 2 | --TODO: Should store all warnings for main application to play back 3 | --TODO: Store every line for writing back. 4 | -- Compiler should only change values, keeping comments intacked. 5 | -- Documentation comments that can be updated could handle user changes. 6 | --]] 7 | local f = {} 8 | 9 | local tonumber, next, type, io, table, string 10 | = tonumber, next, type, io, table, string 11 | local print = print 12 | local error = nil 13 | local sfmt = string.format 14 | local tabcat = table.concat 15 | 16 | f._VERSION = "INI 0.6.3" 17 | 18 | local function lines(str) 19 | return string.gmatch(str, "[^\r\n]*") 20 | end 21 | 22 | function f.parse(inistr, name) 23 | name = name or "line" 24 | --stores all the sections key-value pairs 25 | local t = {} 26 | local section = "" -- default section 27 | local subsection = false 28 | --When slurp-key is set 29 | local slurp_key = false 30 | local slurp_text = {count = 0} 31 | local slurp_comment = false 32 | local linenum = 0 33 | t[section] = {} 34 | for line in lines(inistr) do 35 | linenum = linenum + 1 36 | 37 | local tss = t[section] 38 | if subsection then 39 | tss = tss[subsection] 40 | end 41 | 42 | --grab each line and store it in the table t 43 | if slurp_key then 44 | --looks for ending pattern of multi-line text 45 | local pattern_found = false 46 | if line:find("^%s*%]%]%s*$") then 47 | printf( 48 | "%s:%d WARN: %s.%s: use of ]] is deprecated. Use ]]].", 49 | name, linenum, section, slurp_key 50 | ) 51 | pattern_found = true 52 | end 53 | if line:find("^%s*%]%]%]%s*$") or pattern_found then 54 | --found end pattern, text collection is done 55 | if not slurp_comment and slurp_text.count >= 1 then 56 | local j = slurp_text.count 57 | local text = tabcat(slurp_text, "\n", 1, j) 58 | --trim whitespace (trim6) 59 | text = text:match("^()%s*$") and "" or text:match("^%s*(.*%S)") 60 | tss[slurp_key] = text 61 | end 62 | slurp_text.count = 0 63 | slurp_comment = false 64 | slurp_key = false 65 | elseif not slurp_comment then 66 | --newline 67 | if line:find("^%s*$") then 68 | line = "\n" 69 | end 70 | slurp_text.count = slurp_text.count + 1 71 | slurp_text[slurp_text.count] = line 72 | end 73 | elseif #line > 0 then 74 | local new_section, new_subsec = line:match( 75 | "^%[%s*([%w_]+)%.?([%w_]*)%s*%]%s*$" 76 | ) 77 | if new_subsec and #new_subsec == 0 then new_subsec = false end 78 | 79 | local comment, k, v 80 | if not new_section then 81 | comment, k, v = line:match("^%s*(;?)%s*([%w_]+)%s*=%s*(.-)%s*$") 82 | comment = comment == ";" 83 | end 84 | 85 | if new_section then 86 | if section ~= new_section or not new_subsec then 87 | --t[new_section] will never == false, but for consitency sake 88 | --test for nil here as well as t[new_section][new_subsec] 89 | if type(t[new_section]) ~= "nil" then 90 | return nil, sfmt( 91 | "%s:%d Duplicate section header: %s", 92 | name, linenum, new_section 93 | ) 94 | end 95 | section = new_section 96 | t[section] = {} 97 | end 98 | if new_subsec then 99 | --t[section][subsec] could have matching key == false 100 | --ie. t[section][k] = false 101 | if type(t[new_section][new_subsec]) ~= "nil" then 102 | return nil, sfmt( 103 | "%s:%d Duplicate section header: %s.%s", 104 | name, linenum, section, new_subsec 105 | ) 106 | end 107 | subsection = tonumber(new_subsec) or new_subsec 108 | t[section][subsection] = {} 109 | end 110 | elseif k and v then 111 | local num = tonumber(v) 112 | if num then 113 | v = num 114 | elseif v:find("^%[%[$") then 115 | printf( 116 | "%s:%d WARN: %s.%s: use of [[ is deprecated. Use [[[.", 117 | name, linenum, section, k 118 | ) 119 | --deprecation so it doesn't confuse with behavior of 120 | --Lua's string type. 121 | slurp_key = k 122 | elseif v:find("^%[%[%[") then 123 | --multi-line text found 124 | --Currenly only support the opening braces. Anything else 125 | --on the line should be considered invalid 126 | local s, e, option = v:find("^%[%[%[%s*(.+)%s*$") 127 | if not comment and option then 128 | printf( 129 | "%s:%d ERROR: in %s.%s: block text options are not supported", 130 | name, linenum, section, k, option 131 | ) 132 | comment = true 133 | end 134 | slurp_key = k 135 | slurp_comment = comment 136 | else 137 | local vtl = string.lower(v) 138 | if vtl == "true" then 139 | v = true 140 | elseif vtl == "false" then 141 | v = false 142 | end 143 | end 144 | 145 | if not slurp_key and not comment then 146 | tss[k] = v 147 | end 148 | elseif not line:find("%s*;") then 149 | --garbage data. Should at least warn 150 | printf("%s:%d WARN: Invalid:\n> \"%s\"", name, linenum, line) 151 | end 152 | end 153 | end 154 | return t 155 | end 156 | 157 | function f.write(dest, ini) 158 | error("this write function is garbage and should never be used.") 159 | if type(dest) ~= "string" then return false end 160 | if type(ini) ~= "table" then return false end 161 | local f = io.open(dest, "w") 162 | local section_written = false 163 | local base = ini[""] 164 | ini[""] = nil 165 | 166 | if type(base) == "table" then 167 | for k,v in next, base do 168 | local prefix, postfix = "", "" 169 | section_written = true 170 | if type(v) == "string" and v:find("[\r\n]+") then 171 | --TODO:should also make sure v doesn't contain the postfix 172 | prefix, postfix = "[[[\n", "\n]]]" 173 | end 174 | f:write(sfmt("%s = %s%s%s\n", k, prefix, v, postfix)) 175 | end 176 | end 177 | for k,v in next, ini do 178 | if k ~= "" then 179 | local prefix, postfix = "", "" 180 | if section_written then f:write("\n") end 181 | f:write(sfmt("[%s]\n", k)) 182 | for k2, v2 in next, v do 183 | section_written = true 184 | if type(v2) == "string" and v2:find("[\r\n]+") then 185 | prefix, postfix = "[[[\n", "\n]]]" 186 | end 187 | f:write(sfmt("%s = %s%s%s\n", k2, prefix, v2, postfix)) 188 | end 189 | end 190 | end 191 | f:close() 192 | 193 | return true 194 | end 195 | 196 | return f 197 | -------------------------------------------------------------------------------- /data/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2015 rxi 5 | -- 6 | -- This library is free software; you can redistribute it and/or modify it 7 | -- under the terms of the MIT license. See LICENSE for details. 8 | -- 9 | 10 | local sfmt = string.format 11 | 12 | local json = { _version = "0.1.0" } 13 | 14 | ------------------------------------------------------------------------------- 15 | -- Encode 16 | ------------------------------------------------------------------------------- 17 | 18 | local encode 19 | 20 | local escape_char_map = { 21 | [ "\\" ] = "\\\\", 22 | [ "\"" ] = "\\\"", 23 | [ "\b" ] = "\\b", 24 | [ "\f" ] = "\\f", 25 | [ "\n" ] = "\\n", 26 | [ "\r" ] = "\\r", 27 | [ "\t" ] = "\\t", 28 | } 29 | 30 | local escape_char_map_inv = { [ "\\/" ] = "/" } 31 | for k, v in pairs(escape_char_map) do 32 | escape_char_map_inv[v] = k 33 | end 34 | 35 | 36 | local function escape_char(c) 37 | return escape_char_map[c] or sfmt("\\u%04x", c:byte()) 38 | end 39 | 40 | 41 | local function encode_nil(val) 42 | return "null" 43 | end 44 | 45 | 46 | local function encode_table(val, stack) 47 | local res = {} 48 | stack = stack or {} 49 | 50 | -- Circular reference? 51 | if stack[val] then error("circular reference") end 52 | 53 | stack[val] = true 54 | 55 | if val[1] ~= nil or next(val) == nil then 56 | -- Treat as array -- check keys are valid and it is not sparse 57 | local n = 0 58 | for k in pairs(val) do 59 | if type(k) ~= "number" then 60 | error("invalid table: mixed or invalid key types") 61 | end 62 | n = n + 1 63 | end 64 | if n ~= #val then 65 | error(sfmt("invalid table: sparse array, have %d, expected %d", 66 | #val, n)) 67 | end 68 | -- Encode 69 | for i, v in ipairs(val) do 70 | table.insert(res, encode(v, stack)) 71 | end 72 | stack[val] = nil 73 | return "[" .. table.concat(res, ",") .. "]" 74 | 75 | else 76 | -- Treat as an object 77 | for k, v in pairs(val) do 78 | if type(k) ~= "string" then 79 | error("invalid table: mixed or invalid key types") 80 | end 81 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 82 | end 83 | stack[val] = nil 84 | return "{" .. table.concat(res, ",") .. "}" 85 | end 86 | end 87 | 88 | 89 | local function encode_string(val) 90 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 91 | end 92 | 93 | 94 | local function encode_number(val) 95 | -- Check for NaN, -inf and inf 96 | if val ~= val or val <= -math.huge or val >= math.huge then 97 | error("unexpected number value '" .. tostring(val) .. "'") 98 | end 99 | return sfmt("%.14g", val) 100 | end 101 | 102 | 103 | local type_func_map = { 104 | [ "nil" ] = encode_nil, 105 | [ "table" ] = encode_table, 106 | [ "string" ] = encode_string, 107 | [ "number" ] = encode_number, 108 | [ "boolean" ] = tostring, 109 | } 110 | 111 | 112 | encode = function(val, stack) 113 | local t = type(val) 114 | local f = type_func_map[t] 115 | if f then 116 | return f(val, stack) 117 | end 118 | error("unexpected type '" .. t .. "'") 119 | end 120 | 121 | 122 | function json.encode(val) 123 | return ( encode(val) ) 124 | end 125 | 126 | 127 | ------------------------------------------------------------------------------- 128 | -- Decode 129 | ------------------------------------------------------------------------------- 130 | 131 | local parse 132 | 133 | local function create_set(...) 134 | local res = {} 135 | for i = 1, select("#", ...) do 136 | res[ select(i, ...) ] = true 137 | end 138 | return res 139 | end 140 | 141 | local space_chars = create_set(" ", "\t", "\r", "\n") 142 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 143 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 144 | local literals = create_set("true", "false", "null") 145 | 146 | local literal_map = { 147 | [ "true" ] = true, 148 | [ "false" ] = false, 149 | [ "null" ] = nil, 150 | } 151 | 152 | 153 | local function next_char(str, idx, set, negate) 154 | for i = idx, #str do 155 | if set[str:sub(i, i)] ~= negate then 156 | return i 157 | end 158 | end 159 | return #str + 1 160 | end 161 | 162 | 163 | local function decode_error(str, idx, msg) 164 | local line_count = 1 165 | local col_count = 1 166 | for i = 1, idx - 1 do 167 | col_count = col_count + 1 168 | if str:sub(i, i) == "\n" then 169 | line_count = line_count + 1 170 | col_count = 1 171 | end 172 | end 173 | error( sfmt("%s at line %d col %d", msg, line_count, col_count) ) 174 | end 175 | 176 | 177 | local function codepoint_to_utf8(n) 178 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 179 | local f = math.floor 180 | if n <= 0x7f then 181 | return string.char(n) 182 | elseif n <= 0x7ff then 183 | return string.char(f(n / 64) + 192, n % 64 + 128) 184 | elseif n <= 0xffff then 185 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 186 | elseif n <= 0x10ffff then 187 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 188 | f(n % 4096 / 64) + 128, n % 64 + 128) 189 | end 190 | error( sfmt("invalid unicode codepoint '%x'", n) ) 191 | end 192 | 193 | 194 | local function parse_unicode_escape(s) 195 | local n1 = tonumber( s:sub(3, 6), 16 ) 196 | local n2 = tonumber( s:sub(9, 12), 16 ) 197 | -- Surrogate pair? 198 | if n2 then 199 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 200 | else 201 | return codepoint_to_utf8(n1) 202 | end 203 | end 204 | 205 | 206 | local function parse_string(str, i) 207 | local has_unicode_escape = false 208 | local has_surrogate_escape = false 209 | local has_escape = false 210 | local last 211 | for j = i + 1, #str do 212 | local x = str:byte(j) 213 | 214 | if x < 32 then 215 | decode_error(str, j, "control character in string") 216 | end 217 | 218 | if last == 92 then -- "\\" (escape char) 219 | if x == 117 then -- "u" (unicode escape sequence) 220 | local hex = str:sub(j + 1, j + 5) 221 | if not hex:find("%x%x%x%x") then 222 | decode_error(str, j, "invalid unicode escape in string") 223 | end 224 | if hex:find("^[dD][89aAbB]") then 225 | has_surrogate_escape = true 226 | else 227 | has_unicode_escape = true 228 | end 229 | else 230 | local c = string.char(x) 231 | if not escape_chars[c] then 232 | decode_error(str, j, "invalid escape char '" .. c .. "' in string") 233 | end 234 | has_escape = true 235 | end 236 | last = nil 237 | 238 | elseif x == 34 then -- '"' (end of string) 239 | local s = str:sub(i + 1, j - 1) 240 | if has_surrogate_escape then 241 | s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) 242 | end 243 | if has_unicode_escape then 244 | s = s:gsub("\\u....", parse_unicode_escape) 245 | end 246 | if has_escape then 247 | s = s:gsub("\\.", escape_char_map_inv) 248 | end 249 | return s, j + 1 250 | 251 | else 252 | last = x 253 | end 254 | end 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | key = key:gsub("%-", "_") 324 | -- Read ':' delimiter 325 | i = next_char(str, i, space_chars, true) 326 | if str:sub(i, i) ~= ":" then 327 | decode_error(str, i, "expected ':' after key") 328 | end 329 | i = next_char(str, i + 1, space_chars, true) 330 | -- Read value 331 | val, i = parse(str, i) 332 | -- Set 333 | res[key] = val 334 | -- Next token 335 | i = next_char(str, i, space_chars, true) 336 | local chr = str:sub(i, i) 337 | i = i + 1 338 | if chr == "}" then break end 339 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 340 | end 341 | return res, i 342 | end 343 | 344 | 345 | local char_func_map = { 346 | [ '"' ] = parse_string, 347 | [ "0" ] = parse_number, 348 | [ "1" ] = parse_number, 349 | [ "2" ] = parse_number, 350 | [ "3" ] = parse_number, 351 | [ "4" ] = parse_number, 352 | [ "5" ] = parse_number, 353 | [ "6" ] = parse_number, 354 | [ "7" ] = parse_number, 355 | [ "8" ] = parse_number, 356 | [ "9" ] = parse_number, 357 | [ "-" ] = parse_number, 358 | [ "t" ] = parse_literal, 359 | [ "f" ] = parse_literal, 360 | [ "n" ] = parse_literal, 361 | [ "[" ] = parse_array, 362 | [ "{" ] = parse_object, 363 | } 364 | 365 | 366 | parse = function(str, idx) 367 | local chr = str:sub(idx, idx) 368 | local f = char_func_map[chr] 369 | if f then 370 | return f(str, idx) 371 | end 372 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 373 | end 374 | 375 | 376 | function json.decode(str) 377 | if type(str) ~= "string" then 378 | error("expected argument of type string, got " .. type(str)) 379 | end 380 | return ( parse(str, next_char(str, 1, space_chars, true)) ) 381 | end 382 | 383 | 384 | return json 385 | -------------------------------------------------------------------------------- /data/keys2.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2020 -- Scott Smith -- 2 | ---[ Keys2 ]-------------------------------------------------------------------- 3 | -- Midi Numbers 4 | -- 0xWXYZ 5 | -- * W: always 1, represents "Control Change" 6 | -- * X: channel number (0- 7 | -- * 8 | -------------------------------------------------------------------------------- 9 | local tonumber = tonumber 10 | local floor = math.floor 11 | 12 | local _VERSION = "Keys2 0.5.0" 13 | 14 | local keys2 = {keys = {}, pressed = {}, released = {}, text = {}, all = {}} 15 | do local x, y = love.mouse.getPosition() 16 | keys2.mouseevent = {"moved", x, y, 0, 0} 17 | end 18 | 19 | --{{{--[[ Midi Handling ]]------------------------------------------------------ 20 | local inv127 = 1/127 21 | local _, midi_saved = export("check", "halmidi_saved") 22 | midi_saved = midi_saved or {} 23 | keys2.midi = setmetatable({ 24 | count = 0, size = 2, saved = midi_saved, inv127 = inv127 25 | },{ 26 | __call = function(self, key, multiplier, add) 27 | multiplier, add = multiplier or 1, add or 0 28 | return (self.saved[key] or 63.5) * inv127 * (multiplier) + (add) 29 | end 30 | }) 31 | 32 | if export("check","DEBUG") and DEBUG then 33 | hal.debug.midi_retval = {} 34 | local midimt = getmetatable(keys2.midi) 35 | local call = midimt.__call 36 | function midimt.__call(self, key, ...) 37 | local retval = call(self, key, ...) 38 | hal.debug.midi_retval[key] = retval 39 | return retval 40 | end 41 | end 42 | --}}}--[[ End Midi Handling ]]-------------------------------------------------- 43 | 44 | --[[ Gamepad Table ]]-- {{{ 45 | keys2.gamepad = {count = 0, button = {}, pressed = {}, released = {}} 46 | keys2.gamepad.size = { 47 | axis = 4, 48 | button = 3, 49 | pressed = 3, 50 | released = 3, 51 | } 52 | --[[END: Gamepad Table ]]-- }}} 53 | 54 | keys2.pressed.count = 0 55 | keys2.released.count = 0 56 | keys2.text.count = 0 57 | keys2.all.count = 0 58 | 59 | --the mouse event moved always has the initial position in the stack 60 | --on update, the values are modified inplace on the stack 61 | keys2.mouseevent.count = 5 62 | keys2.mouseevent.size = { 63 | wheel = 3, 64 | moved = 5, 65 | 66 | button = 4, 67 | pressed = 4, 68 | released = 4, 69 | } 70 | 71 | keys2.touchevent = { 72 | count = 0, 73 | touch_count = 0, 74 | moved_i = {}, 75 | 76 | last_idx = 0, 77 | id = {}, 78 | last_x = {}, 79 | last_y = {}, 80 | size = { 81 | moved = 7, 82 | pressed = 5, 83 | released = 5, 84 | } 85 | } 86 | 87 | function keys2:reset() 88 | self.midi.count = 0 89 | self.pressed.count = 0 90 | self.released.count = 0 91 | self.text.count = 0 92 | self.all.count = 0 93 | self.mouseevent.count = 5 94 | --reset dx and dy only for moved event 95 | self.mouseevent[4] = 0 96 | self.mouseevent[5] = 0 97 | 98 | self.gamepad.count = 0 99 | 100 | self.touchevent.count = 0 101 | for i = 1, self.touchevent.touch_count do 102 | self.touchevent.moved_i[i] = -1 103 | end 104 | self.touchevent.touch_count = 0 105 | end 106 | 107 | --{{{ --[[ Input Handling ]]-- 108 | local keys = keys2.keys 109 | 110 | local function love_keypress(tt, key, scancode) 111 | local k2t = keys2[tt] 112 | k2t.count = k2t.count + 1 113 | k2t[k2t.count] = key 114 | 115 | local all = keys2.all 116 | local c = all.count 117 | all.count = c + 2 118 | all[c+1] = tt 119 | all[c+2] = key 120 | end 121 | 122 | local function love_mouseevent(k2t, etype, x, y, button) 123 | local i = k2t.count 124 | if etype ~= "pressed" and etype ~= "released" and etype ~= "wheel" then 125 | error("unrecognized mouse event type, "..etype) 126 | end 127 | k2t.count = i + k2t.size[etype] 128 | 129 | k2t[i+1] = etype --see; /keys2.mouseevent.size 130 | k2t[i+2] = x 131 | k2t[i+3] = y 132 | k2t[i+4] = button --doesn't matter if size < 4 133 | end 134 | 135 | local function love_touchevent(k2t, etype, id, x, y, dx_pressure, dy, pressure) 136 | local i = k2t.count 137 | if etype ~= "moved" and etype ~= "pressed" and etype ~= "released" then 138 | error("unrecognized touch event type, "..etype) 139 | end 140 | local prev_dx, prev_dy = 0, 0 141 | local etype_size = k2t.size[etype] 142 | if etype == "moved" then 143 | if (k2t.moved_i[id] or -1) >= 0 then 144 | --restore previous moved event and deltas 145 | i = k2t.moved_i[id] 146 | prev_dx = k2t[i+5] 147 | prev_dy = k2t[i+6] 148 | --event table won't grow, using previous event 149 | etype_size = 0 150 | else 151 | --store moved event location 152 | k2t.moved_i[id] = i 153 | if k2t.touch_count < id then 154 | k2t.touch_count = id 155 | end 156 | end 157 | end 158 | k2t.count = i + etype_size 159 | 160 | k2t[i+1] = etype 161 | k2t[i+2] = id 162 | k2t[i+3] = x 163 | k2t[i+4] = y 164 | k2t[i+5] = prev_dx + dx_pressure 165 | --like mouseevent, if size < 6, these values will be ignored/overwritten 166 | k2t[i+6] = prev_dy + dy 167 | k2t[i+7] = pressure 168 | end 169 | 170 | local function love_gamepadevent(k2t, etype, id, button_axis, value) 171 | local i = k2t.count 172 | if etype ~= "pressed" and etype ~= "released" and etype ~= "axis" then 173 | error("unrecognized gamepad event type, "..etype) 174 | end 175 | k2t.count = i + k2t.size[etype] 176 | 177 | k2t[i+1] = etype --see; keys2.gamepad.size 178 | k2t[i+2] = id 179 | k2t[i+3] = sfmt("gp%d_%s", id, button_axis) 180 | k2t[i+4] = value --doesn't matter if size < 4 181 | end 182 | 183 | function keys2.getHandlers() 184 | local handlers = {} 185 | function handlers.filedropped(file) 186 | printf("File dropped: \"%s\"\n", file:getFilename()) 187 | end 188 | function handlers.directorydropped(name) 189 | printf("Directory dropped: \"%s\"\n", name) 190 | end 191 | function handlers.keypressed(key) 192 | keys[key] = true 193 | love_keypress("pressed", key) 194 | end 195 | 196 | function handlers.keyreleased(key) 197 | keys[key] = false 198 | love_keypress("released", key) 199 | end 200 | 201 | function handlers.textinput(text) 202 | love_keypress("text", text) 203 | end 204 | 205 | function handlers.mousemoved(x, y, dx, dy) 206 | local me = keys2.mouseevent 207 | if me[1] == "moved" then 208 | me[2] = x 209 | me[3] = y 210 | me[4] = me[4] + dx 211 | me[5] = me[5] + dy 212 | else 213 | error("keys2.mouseevent[1] must always be \"moved\"") 214 | end 215 | end 216 | 217 | function handlers.mousepressed(x, y, button) 218 | love_mouseevent(keys2.mouseevent, "pressed", x, y, button) 219 | end 220 | 221 | function handlers.mousereleased(x, y, button) 222 | love_mouseevent(keys2.mouseevent, "released", x, y, button) 223 | end 224 | 225 | --TODO: handler focus won't register properly. have to register in akpack 226 | function handlers.focus(f) 227 | print("Focused:", f) 228 | end 229 | 230 | function handlers.wheelmoved(x,y) 231 | love_mouseevent(keys2.mouseevent, "wheel", x, y) 232 | end 233 | 234 | local touchevents = keys2.touchevent 235 | function handlers.touchpressed(id, x, y, dx, dy, pressure) 236 | x = floor(x + 0.5) 237 | y = floor(y + 0.5) 238 | local index = touchevents.last_idx + 1 239 | for i = 1, index - 1 do 240 | if not touchevents.id[i] then 241 | index = i 242 | break 243 | end 244 | end 245 | if index > touchevents.last_idx then 246 | touchevents.last_idx = index 247 | end 248 | touchevents.id[index] = id 249 | touchevents.last_x[index] = x 250 | touchevents.last_y[index] = y 251 | 252 | pressure = pressure or 1.0 253 | 254 | love_touchevent(touchevents, "pressed", index, x, y, pressure, 0, 0) 255 | end 256 | 257 | function handlers.touchreleased(id, x, y, dx, dy, pressure) 258 | x = floor(x + 0.5) 259 | y = floor(y + 0.5) 260 | local index = touchevents.last_idx 261 | for i = 1, index do 262 | if touchevents.id[i] == id then 263 | touchevents.id[i] = false 264 | if i == index then 265 | touchevents.last_idx = index - 1 266 | end 267 | index = i 268 | break 269 | end 270 | end 271 | pressure = pressure or 0.0 272 | 273 | love_touchevent(touchevents, "released", index, x, y, pressure, 0, 0) 274 | end 275 | 276 | function handlers.touchmoved(id, x, y, dx, dy, pressure) 277 | x = floor(x + 0.5) 278 | y = floor(y + 0.5) 279 | local index = touchevents.last_idx 280 | for i = 1, index do 281 | if touchevents.id[i] == id then 282 | index = i 283 | break 284 | end 285 | end 286 | --dy is nul for some reason so I have to figure it out myself 287 | --also figure I might as well do it for dx while I'm at it 288 | dx = x - touchevents.last_x[index] 289 | dy = y - touchevents.last_y[index] 290 | touchevents.last_x[index] = x 291 | touchevents.last_y[index] = y 292 | 293 | pressure = pressure or 1.0 294 | 295 | love_touchevent(touchevents, "moved", index, x, y, dx, dy, pressure) 296 | end 297 | 298 | function handlers.gamepadpressed(gp, button) 299 | love_gamepadevent(keys2.gamepad, "pressed", gp:getID(), button) 300 | end 301 | 302 | function handlers.gamepadreleased(gp, button) 303 | love_gamepadevent(keys2.gamepad, "released", gp:getID(), button) 304 | end 305 | 306 | function handlers.gamepadaxis(gp, axis, value) 307 | love_gamepadevent(keys2.gamepad, "axis", gp:getID(), axis, value) 308 | end 309 | 310 | return handlers 311 | end 312 | 313 | do --initialize love callback handlers 314 | local handlers = keys2.getHandlers() 315 | for k,v in next, handlers do 316 | love[k] = v 317 | end 318 | end 319 | 320 | local function love_midievent(k2t, etype, channel, midikey, value, a3) 321 | local key = 0 322 | a3 = a3 or 0 323 | local alsa = require"midialsa" 324 | if etype == "control change" then 325 | local controller = midikey 326 | a3 = 0 327 | key = 0xB000 328 | elseif etype == "note on" then 329 | local pitch, velocity, duration = midikey, value, a3 330 | if midikey == 0 then 331 | alsa.output(alsa.controllerevent(0, 9, 27)) 332 | end 333 | key = 0x8000 334 | elseif etype == "note off" then 335 | local pitch, velocity, duration = midikey, value, a3 336 | if midikey == 0 then 337 | alsa.output(alsa.controllerevent(0xa, 1, midi_saved[0xba01] or 0)) 338 | end 339 | key = 0x9000 340 | elseif etype == "program change" then 341 | midikey = 0 342 | key = 0xC000 343 | end 344 | if key == 0 then 345 | error("unrecognized midi event type!") 346 | end 347 | 348 | local i = k2t.count 349 | k2t.count = i + k2t.size --size should be 2 350 | 351 | --0x1234 (key == 1, channel == 2, controller == 34) 352 | --key = key + (channel * 0x100) + controller 353 | key = key + (channel * 0x100) + midikey 354 | printf("0x%04x %3d : %02x %02x %02x %s", 355 | key, value, midikey, value, a3, etype 356 | ) 357 | k2t[i+1] = key 358 | k2t[i+2] = value 359 | k2t.saved[key] = value 360 | end 361 | 362 | local midi_receive 363 | if hal_conf.midi and love.system.getOS() == "Linux" then 364 | local alsa = require "midialsa" 365 | local inv127 = 1/127 366 | midi_receive = function() 367 | if not alsa.inputpending() then return end 368 | while alsa.inputpending() > 0 do 369 | local amidi_in = alsa.input() 370 | local evtype = amidi_in[1] 371 | if evtype == alsa.SND_SEQ_EVENT_PORT_UNSUBSCRIBED then break end 372 | 373 | local dat = amidi_in[8] 374 | if evtype == alsa.SND_SEQ_EVENT_CONTROLLER then 375 | love_midievent(keys2.midi, "control change", dat[1],dat[5],dat[6]) 376 | elseif evtype == alsa.SND_SEQ_EVENT_NOTEON then 377 | love_midievent(keys2.midi, "note on", dat[1],dat[2],dat[3],dat[5]) 378 | elseif evtype == alsa.SND_SEQ_EVENT_NOTEOFF then 379 | love_midievent(keys2.midi, "note off", dat[1],dat[2],dat[3],dat[5]) 380 | elseif evtype == alsa.SND_SEQ_EVENT_PGMCHANGE then 381 | love_midievent(keys2.midi, "program change", dat[1], 0, dat[6]) 382 | else 383 | for k, v in next, alsa do 384 | if evtype == v then 385 | print("evtype", #dat, k) 386 | for i = 1, #dat do 387 | print("", i, dat[i]) 388 | end 389 | break 390 | end 391 | end 392 | end 393 | end 394 | end 395 | else 396 | midi_receive = function() end 397 | print("MIDI system not supported.") 398 | end 399 | 400 | --}}} 401 | function keys2.doevents() 402 | midi_receive() 403 | end 404 | 405 | return keys2 406 | -------------------------------------------------------------------------------- /data/main2.lua: -------------------------------------------------------------------------------- 1 | -- Copywrite 2020 -- Scott Smith -- 2 | 3 | local export = export 4 | export. hal = {conf = hal_conf} 5 | hal.debug = {} 6 | export. keys2 = require "keys2" 7 | --{{{populate hal_conf.AG 8 | do 9 | local game_config = "game.cfg" 10 | local data = require"utils.io".open("./"..game_config, "r") 11 | if data then 12 | local idat = require"ini".parse(data:read("*a"), game_config) 13 | data:close() 14 | hal_conf.AG = idat[""] 15 | else 16 | error("missing "..game_config) 17 | end 18 | end 19 | --}}}populate hal_conf.AG 20 | local UPDATES_PER_SECOND = 60--24 --20.0 21 | do 22 | local fps = hal_conf.AG.FPS 23 | if type(fps) == "number" and fps ~= 0 then 24 | if fps < 10 then 25 | fps = 10 26 | elseif fps > 60 then 27 | fps = 60 28 | end 29 | UPDATES_PER_SECOND = fps 30 | end 31 | end 32 | local UPDATE_DT = 1/UPDATES_PER_SECOND 33 | local TARGET_DT = 60 --target minimum 60fps 34 | local invTARGET_DT = 1/TARGET_DT 35 | local FRAME_LIMIT = UPDATES_PER_SECOND + 0.5 36 | local invFRAME_LIMIT = FRAME_LIMIT == 0 and 0 or 1/FRAME_LIMIT 37 | 38 | --The Main Loop functions 39 | 40 | --the number is abitrary and only has meaning in the context of this program 41 | --that meaning I am unsure of though 42 | local GC_STEP_SIZE = 1 43 | local GC_UPDATES_PER_SECOND = 20 44 | 45 | 46 | --{{{ [[ local variable assignment ]] 47 | local keys2 = keys2 48 | local keys = keys2.keys 49 | local midi = keys2.midi 50 | 51 | local inputSystem = require "sysinput" 52 | 53 | local ffi = require "ffi" 54 | local ini = require "ini" 55 | local string, error, loadfile, math, love 56 | = string, error, loadfile, math, love 57 | local strfmt, unpack = string.format, unpack 58 | 59 | local floor, ceil, min = math.floor, math.ceil, math.min 60 | local atan2, cos, sin = math.atan2, math.cos, math.sin 61 | local abs, sqrt = math.abs, math.sqrt 62 | local average = require"utils.math.stats".average 63 | local stddev = require"utils.math.stats".stddev 64 | local round = require"utils.math.round" 65 | 66 | local lg = love.graphics 67 | 68 | 69 | --The following shouldn't need to change very often. 70 | --all code should be put in functions above. 71 | 72 | --Escape key will reload game when pressed. Long press quits game. 73 | --this function should always be able to be called. 74 | local levent, leventpump, leventpoll, love_handlers 75 | = love.event, love.event.pump, love.event.poll, love.handlers 76 | local ltimer, ltimerstep, ltimergetdelta, ltimersleep 77 | = love.timer, love.timer.step, love.timer.getDelta, love.timer.sleep 78 | local lwindowisopen, lwindowgetWidth, lw = 79 | love.window.isOpen, love.window.getWidth, love.window 80 | local lgprint = lg.print 81 | 82 | --}}} 83 | 84 | --{{{ DEBUG font 85 | local debug_font 86 | local function debug_font_setup() 87 | debug_font = lg.newImageFont("assets/akciom-4x9.png", 88 | " !\"#$%&'()*+,-./0123456789:;<=>?".. 89 | "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_".. 90 | "`abcdefghijklmnopqrstuvwxyz{|}~", 91 | 1) 92 | end 93 | --}}} DEBUG font 94 | 95 | local escapekey do --{{{ --[[ Escape Key ]] 96 | local quit_timer = 0 97 | local function esc_quit() --{{{ 98 | lg.reset() 99 | lg.clear() --Good bye 100 | lgprint("Good bye!", 10, 10) 101 | lg.present() 102 | ltimersleep(0.5) 103 | love.event.quit() 104 | end--}}} 105 | local function esc_reload()--{{{ 106 | love.event.push("reload") 107 | end--}}} 108 | local function esc_refresh()--{{{ 109 | love.event.push("refresh") 110 | end--}}} 111 | local function esc_noop() end 112 | 113 | local on_hold = esc_refresh 114 | local on_release = esc_noop 115 | 116 | function escapekey(dt) 117 | if midi(0xb02a) > 0.5 then --stop 118 | midi.saved._hashalted = true 119 | error("HALT_THE_GAME_PLEASE") 120 | end 121 | if keys.escape then 122 | quit_timer = quit_timer + dt 123 | if quit_timer > 0.3228 then 124 | on_hold() 125 | end 126 | end 127 | local rewind = midi(0xb02b) > 0.5 --pressed or released 128 | if not midi.saved._hasreset and rewind then 129 | midi.saved._hasreset = true 130 | love.event.push("reload") 131 | elseif not rewind then 132 | midi.saved._hasreset = false 133 | end 134 | local kr = keys2.released 135 | for i = 1, kr.count do 136 | if kr[i] == "escape" then 137 | quit_timer = 0 138 | on_release() 139 | end 140 | end 141 | end 142 | end --}}} 143 | 144 | function love.focus(f) 145 | lw.setDisplaySleepEnabled(true) 146 | end 147 | 148 | local DEBUG_DISPLAY_MIDI_TOGGLE = false 149 | 150 | 151 | local profile = require "debug.Profile" 152 | --{{{ PROFILE SETUP 153 | do 154 | local p = profile 155 | p( "main", "Main Loop") 156 | p.set.main( p("start", " Event Loop")) 157 | p.set.main( p("start_process"," Event Process")) 158 | 159 | p.set.main( p("accumulator", " Accumulator Loop")) 160 | p.set.main(p ("garbage", " GC")) 161 | 162 | p( "update", " Update Game") 163 | p.set.update(p("inputsys", " Input System")) 164 | 165 | p( "render", " Render Loop") 166 | p.set.render(p("render2", " Debug Output")) 167 | p.set.render(p("render3", " Present!")) 168 | p.set.render(p("render1", " Render Game")) 169 | p( "sleep", "Sleeping") 170 | end 171 | --}}} PROFILE SETUP 172 | 173 | local function main(arg) 174 | --{{{ --[[ Pre-Initalization ]]-- 175 | if not love.math then error("Need some love.math") end 176 | if not love.event then error("Need some love.event") end 177 | if not love.timer then error("Need some love.timer") end 178 | 179 | --reset events 180 | keys2:reset() 181 | love.event.clear() 182 | 183 | local hal = hal 184 | hal.met = 0 185 | hal.frame = 0 186 | 187 | --Set Seed 188 | love.math.setRandomSeed(os.time()) 189 | --add some set randomness 190 | do 191 | local lmr = love.math.random 192 | local count = 0 193 | local randval 194 | for i = 1, 10 do 195 | count = count + 1 196 | randval = lmr(40) 197 | if randval == count + 15 then 198 | break 199 | end 200 | end 201 | for i = 1, randval + count do 202 | lmr() 203 | end 204 | 205 | end 206 | --end:Set Seed 207 | 208 | local pause_game = false 209 | --Counting Frames and Timers 210 | local frames = ffi.new("uint16_t [?]", UPDATES_PER_SECOND+1) 211 | local frametime = 0.0 --total time simulation has been running 212 | local fps_update = 1 213 | local fps = 0 214 | local fps_min = math.huge 215 | local fps_max = 0 216 | local fps_reset_clock = hal.met + 1 + UPDATE_DT 217 | local accumulator = 0.0 218 | local frame_table = { 219 | count = 0, 220 | max = UPDATES_PER_SECOND * 5, 221 | pointer = 0, 222 | } 223 | local frame_average = 0.0 224 | local frame_average_percentage = 0.0 225 | local frame_std_dev_table = {} 226 | local frame_std_dev = false 227 | local frame_spikes = { 228 | count = 0, 229 | strings = {max = 10}, 230 | average = 0/0, stddev = 0 231 | } 232 | local frame_limit_percentage = 0 233 | local frame_limit_time = 0 234 | local frame_limit_percentage_max = 0 235 | local frame_limit_time_max = 0 236 | local target_frame_percentage = 0 237 | local target_frame_percentage_max = 0 238 | local garbage_time = { 239 | count = 0, 240 | pointer = 0, 241 | count_max = 9999, 242 | min = math.huge, 243 | max = 0, 244 | average = 0, 245 | stddev = 0, 246 | 247 | run_gc = 0, 248 | run_gc_default = ceil(UPDATES_PER_SECOND / GC_UPDATES_PER_SECOND), 249 | 250 | steps_between = 0, 251 | steps_between_complete = -1, 252 | } 253 | local allocated_memory = 0 254 | local allocated_memory_max = 0 255 | local allocated_memory_frames = {update = 1} 256 | --end:Counting Frames and Timers 257 | --}}} 258 | --TODO:These commands are ripe for abuse when inputs are user defined 259 | local update, render do --{{{ [ Setup include() and initialize ] 260 | 261 | local entryfile = hal.conf.AG.PACK 262 | 263 | local suffix = ".agpack" 264 | local entryfile_match_string = "^([%w_%-]+)$" 265 | 266 | do 267 | local errentryfile = entryfile 268 | entryfile = string.match(entryfile, entryfile_match_string) 269 | if not entryfile then 270 | error("entryfile is invalid, %s", errentryfile) 271 | end 272 | end 273 | 274 | export .include = function(incfile) 275 | local errincfile = incfile 276 | incfile = string.match(incfile, entryfile_match_string) 277 | if not incfile then 278 | error(strfmt("include: invalid file name, %s", errincfile)) 279 | end 280 | local fullincfile = strfmt("./%s%s/src/%s.lua", 281 | entryfile, suffix, incfile 282 | ) 283 | local incfunc = assert(loadfile(fullincfile)) 284 | 285 | --like require, include only returns one thing 286 | return incfunc(incfile), nil 287 | end 288 | --same as dofile(filename)() but adding entryfile as first argument 289 | local loadfilename = strfmt("./%s%s/main.lua", entryfile, suffix) 290 | local entry = assert(loadfile(loadfilename)) 291 | do --load keybindings 292 | local kbfn = strfmt("./%s%s/keybind.cfg", entryfile, suffix) 293 | local f = io.open(kbfn) 294 | if f then 295 | hal.input_load_bindings(assert(ini.parse(f:read("*a")))) 296 | f:close() 297 | end 298 | end 299 | 300 | local initialize 301 | initialize, update, render = entry(entryfile)() 302 | if type(initialize) ~= "function" 303 | and type(update) ~= "function" 304 | and type(render) ~= "function" 305 | then 306 | error("invalid entry file") 307 | end 308 | 309 | initialize(arg) 310 | end--}}} 311 | 312 | --{{{ Process delay based on window focus 313 | local process_delay = 0 314 | local process_delay_default = floor(UPDATES_PER_SECOND / 3) 315 | local process_delay_mouse = 0 316 | local process_delay_mouse_default = floor(UPDATES_PER_SECOND / 20) 317 | local lwHasFocus = love.window.hasFocus 318 | local lwHasMouseFocus = love.window.hasMouseFocus 319 | --}}} Process delay based on window focus 320 | 321 | --log.hal"HAL! " 322 | --printf " |" 323 | --for k,v in next, hal do 324 | -- printf("%s|", k) 325 | --end 326 | --printf "\n" 327 | 328 | -- We don't want the first frame's dt to include time taken to initialize. 329 | ltimerstep() 330 | local gettime = ltimer.getTime 331 | while true do 332 | profile.main[1]:start() 333 | profile.start:start() 334 | local this_frame_time = gettime() 335 | local updated = false 336 | 337 | --{{{ [[ Process events ]] 338 | local process = false 339 | if lwHasFocus() then 340 | process = true 341 | elseif lwHasMouseFocus() then 342 | process_delay_mouse = process_delay_mouse - 1 343 | if process_delay_mouse <= 0 then 344 | process = true 345 | process_delay_mouse = process_delay_mouse_default 346 | end 347 | else 348 | process_delay = process_delay - 1 349 | if process_delay <= 0 then 350 | process = true 351 | process_delay = process_delay_default 352 | end 353 | end 354 | 355 | 356 | if process then 357 | profile.start_process:start() 358 | keys2.doevents() 359 | leventpump() 360 | --do e,a,b,c,d = love.event.wait() 361 | for e,a,b,c,d in leventpoll() do 362 | love_handlers[e](a,b,c,d) 363 | end 364 | profile.start_process:lap() 365 | end 366 | --}}}end:process events 367 | 368 | -- Update dt, as we'll be passing it to update 369 | ltimerstep() 370 | frametime = ltimergetdelta() 371 | accumulator = accumulator + frametime 372 | profile.start:lap() 373 | while accumulator >= UPDATE_DT do 374 | profile.accumulator:start() 375 | profile.update[1]:start() 376 | hal.input.processed = process 377 | if process then 378 | --TODO:escapekey shouldn't be available in release 379 | profile.inputsys:start() 380 | escapekey(UPDATE_DT) 381 | inputSystem(UPDATE_DT) 382 | profile.inputsys:lap() 383 | end 384 | --pause 385 | local midiplaypause = midi(0xb029) > 0.5 386 | if not midi.saved._haspaused and midiplaypause then 387 | pause_game = not pause_game and accumulator 388 | midi.saved._haspaused = true 389 | elseif not midiplaypause then 390 | midi.saved._haspaused = false 391 | end 392 | if not pause_game then 393 | update(UPDATE_DT) 394 | 395 | hal.met = hal.met + UPDATE_DT 396 | fps_update = fps_update + 1 397 | if fps_update > UPDATES_PER_SECOND then 398 | fps_update = 1 399 | end 400 | -- frame update reported to use a 0 based 401 | hal.frame = fps_update - 1 402 | end 403 | 404 | if hal.input.debug_menu == "pressed" or 405 | (midi(0xb02e) == 1 and not DEBUG_DISPLAY_MIDI_TOGGLE) 406 | then 407 | DEBUG_DISPLAY_MIDI_TOGGLE = true 408 | hal.debug_display_power = true 409 | hal.debug_display = not hal.debug_display 410 | end 411 | if DEBUG_DISPLAY_MIDI_TOGGLE then 412 | DEBUG_DISPLAY_MIDI_TOGGLE = midi(0xb02e) == 1 413 | end 414 | 415 | 416 | keys2:reset() 417 | 418 | if accumulator > 5.0 then 419 | hal.accumulator_reset = accumulator 420 | accumulator = UPDATE_DT 421 | end 422 | accumulator = accumulator - UPDATE_DT 423 | 424 | --{{{ [[ DEBUG Update/FPS ]]-- 425 | if hal.debug_display then 426 | updated = true --used for debug rendering 427 | 428 | fps = 0 429 | for i = 1, UPDATES_PER_SECOND do 430 | fps = fps + frames[i] 431 | end 432 | frames[fps_update] = 0 433 | 434 | if fps_reset_clock then 435 | if hal.met >= fps_reset_clock then 436 | fps_reset_clock = false 437 | fps_min = fps 438 | fps_max = fps 439 | else 440 | fps, fps_min, fps_max = 0, 0, 0 441 | end 442 | end 443 | if fps < fps_min then fps_min = fps end 444 | if fps > fps_max then fps_max = fps end 445 | 446 | --get smooth avg of allocated memory (reported by collectgarbage("count")) 447 | local amfu = allocated_memory_frames.update + 1 448 | if amfu > UPDATES_PER_SECOND then 449 | amfu = 1 450 | end 451 | allocated_memory_frames.update = amfu 452 | allocated_memory_frames[amfu] = collectgarbage("count") 453 | allocated_memory = 0 454 | for i = 1, UPDATES_PER_SECOND do 455 | allocated_memory = allocated_memory + (allocated_memory_frames[i] or 0) 456 | end 457 | allocated_memory = allocated_memory * (UPDATE_DT) 458 | end --}}} end:DEBUG Update/FPS 459 | 460 | profile.update[1]:lap() 461 | garbage_time.run_gc = garbage_time.run_gc - 1 462 | if garbage_time.run_gc <= 0 then 463 | garbage_time.run_gc = garbage_time.run_gc_default 464 | profile.garbage:start() 465 | --seems like a good place to run a gc step 466 | local gc_start = gettime() 467 | garbage_time.steps_between = 468 | garbage_time.steps_between + 1 469 | if collectgarbage("step", GC_STEP_SIZE) then 470 | --print("GC Steps", garbage_time.steps_between_complete) 471 | garbage_time.steps_between_complete = 472 | garbage_time.steps_between 473 | garbage_time.steps_between = 0 474 | end 475 | collectgarbage("stop") 476 | local p = garbage_time.pointer + 1 477 | local c = garbage_time.count 478 | if p > garbage_time.count_max then 479 | p = 1 480 | end 481 | if c < p then garbage_time.count = p end 482 | garbage_time.pointer = p 483 | local gc_end = gettime() - gc_start 484 | garbage_time[p] = gc_end 485 | if gc_end > garbage_time.max then 486 | garbage_time.max = gc_end 487 | end 488 | if gc_end < garbage_time.min then 489 | garbage_time.min = gc_end 490 | end 491 | profile.garbage:lap() 492 | end 493 | 494 | profile.accumulator:lap() 495 | end 496 | 497 | profile.render[1]:start() 498 | if lwindowisopen() then 499 | profile.render1:start() 500 | --lg.clear(lg.getBackgroundColor()) 501 | lg.origin() 502 | 503 | local alpha = accumulator * UPDATES_PER_SECOND 504 | if pause_game then 505 | alpha = pause_game * UPDATES_PER_SECOND 506 | end 507 | local present = render(alpha) 508 | profile.render1:lap() 509 | 510 | --{{{ [[ DEBUG Rendering ]]-- 511 | if hal.debug_display then 512 | profile.render2:start() 513 | present = true 514 | local poweredon = hal.debug_display_power 515 | local met = hal.met 516 | local wmod = 140 517 | 518 | --reset fps counters, data is probably old/useless and needs to be reset 519 | if poweredon then 520 | fps_reset_clock = met + 1 + UPDATE_DT 521 | frame_limit_time_max = 0 522 | frame_limit_percentage_max = 0 523 | frame_table.count = 0 524 | frame_table.pointer = 0 525 | frame_spikes.count = 0 526 | for i = 1, #frame_spikes.strings do 527 | frame_spikes.strings[i] = nil 528 | end 529 | if not debug_font then debug_font_setup() end 530 | end 531 | local old_font = lg.getFont() 532 | lg.setFont(debug_font) 533 | 534 | if updated then 535 | if allocated_memory > allocated_memory_max then 536 | allocated_memory_max = allocated_memory 537 | end 538 | local flt = gettime() - this_frame_time 539 | do 540 | local p = frame_table.pointer + 1 541 | local c = frame_table.count 542 | local calc_std_dev = false 543 | if not frame_std_dev and p > UPDATES_PER_SECOND then 544 | calc_std_dev = true 545 | end 546 | if p > frame_table.max then 547 | calc_std_dev = true 548 | frame_table.count = frame_table.max 549 | p = 1 550 | end 551 | if c < p then c, frame_table.count = p, p end 552 | frame_table.pointer = p 553 | frame_table[p] = flt 554 | local avg = average(frame_table) 555 | frame_average = avg * 1000 556 | frame_average_percentage = avg * TARGET_DT * 100 557 | local std_dev_multi = 5 558 | garbage_time.average = average(garbage_time) 559 | if calc_std_dev then 560 | garbage_time.stddev = stddev(garbage_time, garbage_time.average) 561 | local std_dev = stddev(frame_table, avg) 562 | local avg_dev_high = avg + std_dev * std_dev_multi 563 | if not frame_std_dev then 564 | frame_std_dev = std_dev * 1000 565 | end 566 | frame_std_dev = (std_dev * 1000 * 0.8) + (frame_std_dev * 0.2) 567 | frame_spikes.average = average(frame_spikes) 568 | frame_spikes.stddev = stddev(frame_spikes, frame_spikes.average) 569 | elseif frame_std_dev then 570 | local flt1000 = flt * 1000 571 | local high = frame_average + frame_std_dev * std_dev_multi 572 | if flt1000 > high then 573 | local si = frame_spikes.count + 1 574 | frame_spikes[si] = flt1000 575 | frame_spikes.count = si 576 | local spike_format_str = "%7.2fs|%03d:%5.2fms,%5.2fms" 577 | table.insert(frame_spikes.strings, strfmt(spike_format_str, 578 | hal.met, si, flt1000, 579 | garbage_time[garbage_time.count] * 1000 580 | )) 581 | end 582 | frame_spikes.average = average(frame_spikes) 583 | end 584 | for i = frame_spikes.strings.max+1, #frame_spikes.strings do 585 | table.remove(frame_spikes.strings, 1) 586 | end 587 | end 588 | if flt > frame_limit_time_max then 589 | frame_limit_time_max = flt 590 | frame_limit_percentage_max = flt * FRAME_LIMIT * 100 591 | target_frame_percentage_max = flt * TARGET_DT * 100 592 | end 593 | frame_limit_time = (frame_limit_time * 0.2) + (flt * 0.8) 594 | frame_limit_percentage = frame_limit_percentage *0.2 + 595 | (flt * FRAME_LIMIT * 100) *0.8 596 | target_frame_percentage = target_frame_percentage * 0.2 + 597 | (flt * TARGET_DT * 100) * 0.8 598 | end 599 | local y_position = 0 600 | local function y_pos(multi) 601 | local y = y_position 602 | y_position = y + round((multi or 1) * 9) 603 | return y 604 | end 605 | local w, h = lg.getDimensions() 606 | local save_r,save_b,save_g,save_a = lg.getColor() 607 | lg.setColor(0.0627, 0.0392, 0.0627, 0.77) 608 | lg.rectangle("fill", w-wmod-4, 0,wmod+4, h) 609 | lg.setColor(0.9882,0.8706,0.9882) 610 | lgprint(strfmt("%4s (%3s,%3s) %7.2fs", 611 | fps, floor(fps_min), floor(fps_max), met), w-wmod, y_pos()) 612 | do --{{{ print average and std dev 613 | local avg_std_dev_str = "%5.2fms avg,%4.2fms std dev" 614 | local count_or_std_dev 615 | if frame_std_dev then 616 | count_or_std_dev = frame_std_dev 617 | else 618 | avg_std_dev_str = "%5.2fms avg,%3d frames remain" 619 | count_or_std_dev = UPDATES_PER_SECOND - frame_table.count 620 | end 621 | lgprint(strfmt(avg_std_dev_str, frame_average, count_or_std_dev), 622 | w-wmod, y_pos()) 623 | lgprint(strfmt(" %5.1f%%of%4.1fms Avg", 624 | frame_average_percentage, invTARGET_DT*1000), 625 | w-wmod, y_pos()) 626 | y_pos(0.2) 627 | end--}}} 628 | lgprint(strfmt("%3d GC Steps till complete", 629 | garbage_time.steps_between_complete), 630 | w-wmod, y_pos()) 631 | lgprint(strfmt("%5.2fms avg,%4.2fms SD GC%4d", 632 | garbage_time.average * 1000, 633 | garbage_time.stddev * 1000, garbage_time.count), 634 | w-wmod, y_pos()) 635 | lgprint(strfmt("%5.2fms MAX,%4.2fms MIN GC", 636 | garbage_time.max * 1000, garbage_time.min * 1000), 637 | w-wmod, y_pos()) 638 | y_pos(.2) 639 | lgprint(strfmt("%5.2fms on,%5.2fms off", 640 | frame_limit_time * 1000, 641 | (invFRAME_LIMIT - (gettime() - this_frame_time)) * 1000), 642 | w-wmod, y_pos()) 643 | lgprint(strfmt("%5.2fms on,%5.2fms off MAX", 644 | frame_limit_time_max * 1000, 645 | (invFRAME_LIMIT - (frame_limit_time_max)) * 1000), 646 | w-wmod, y_pos()) 647 | lgprint(strfmt(" %5.1f%%of%4.1fms", 648 | target_frame_percentage, invTARGET_DT*1000), 649 | w-wmod, y_pos()) 650 | lgprint(strfmt(" %5.1f%%of%4.1fms MAX", 651 | target_frame_percentage_max, invTARGET_DT*1000), 652 | w-wmod, y_pos()) 653 | if FRAME_LIMIT ~= TARGET_DT then 654 | lgprint(strfmt(" %5.1f%%of%4.1fms", 655 | frame_limit_percentage, invFRAME_LIMIT*1000), 656 | w-wmod, y_pos()) 657 | lgprint(strfmt(" %5.1f%%of%4.1fms MAX", 658 | frame_limit_percentage_max, invFRAME_LIMIT*1000), 659 | w-wmod, y_pos()) 660 | end 661 | lgprint(strfmt("Mem:%10.2fKB%10.2fKB", 662 | allocated_memory, allocated_memory_max), 663 | w-wmod, y_pos()) 664 | y_pos(.5) 665 | 666 | if frame_spikes.count > 0 then 667 | lgprint("____________________________", w-wmod, y_position+2) 668 | lgprint(strfmt("Spikes: %3d Frame GC", frame_spikes.count), 669 | w-wmod, y_pos(1.1)) 670 | for i = 1, #frame_spikes.strings do 671 | lgprint(frame_spikes.strings[i], 672 | w-wmod, y_pos()) 673 | end 674 | y_pos(0.2) 675 | lgprint("____________________________", w-wmod, y_position+2) 676 | lgprint(strfmt(" %5.2fms avg,%5.2fms stddev", 677 | frame_spikes.average, frame_spikes.stddev), 678 | w-wmod, y_pos() 679 | ) 680 | end 681 | 682 | if hal_conf.midi and love.system.getOS() == "Linux" then 683 | if poweredon then 684 | --midi isn't tracked when debug display is off 685 | --this basically resets it 686 | hal.debug.midi_key = 0 687 | hal.debug.midi_val = 0 688 | end 689 | local m = keys2.midi 690 | local i = m.count - 1 691 | if i >= 1 then 692 | hal.debug.midi_key = m[i] 693 | hal.debug.midi_val = m[i+1] 694 | end 695 | local key = hal.debug.midi_key or 0 696 | local val = (hal.debug.midi_val or 0) * midi.inv127 697 | local rtv = hal.debug.midi_retval[key] or 0 698 | 699 | local str 700 | if key == 0 then 701 | str = "Midi --no recent input--" 702 | else 703 | str = strfmt("Midi 0x%04x %5.3f; %9.2f", key, val, rtv) 704 | end 705 | y_pos() 706 | lgprint(str, w-wmod, y_pos()) 707 | lgprint(pause_game and " -- Paused --" or "", w-wmod, y_pos()) 708 | end 709 | hal.debug_display_power = false --done powering on 710 | lg.setColor(save_r,save_g,save_b,save_a) 711 | lg.setFont(old_font) 712 | profile.render2:lap() 713 | end 714 | --}}}end:DEBUG Rendering 715 | 716 | if present then 717 | profile.render3:start() 718 | lg.present() 719 | profile.render3:lap() 720 | end 721 | frames[fps_update] = frames[fps_update] + 1 722 | end 723 | profile.render[1]:lap() 724 | 725 | profile.main[1]:lap() 726 | 727 | profile.sleep[1]:start() 728 | ltimersleep(invFRAME_LIMIT - (gettime() - this_frame_time)) 729 | profile.sleep[1]:lap() 730 | end 731 | end 732 | 733 | return main 734 | -------------------------------------------------------------------------------- /data/sap.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- SAP Library -- String Argument Parser 3 | -- Copyright 2020 - Scott Smith 4 | -------------------------------------------------------------------------------- 5 | 6 | local _VERSION = "SAP 0.4.0-alpha.1" 7 | 8 | --local _ 9 | 10 | local sfind = string.find 11 | local ssub = string.sub 12 | local sfmt = string.format 13 | local tostring, type, select, error, table, setmetatable 14 | = tostring, type, select, error, table, setmetatable 15 | 16 | local function replace_variable(str, pos, replacement_data) --{{{ 17 | --find short version of string func 18 | local _, e, match = sfind(str, "^([%w_]+)", pos) 19 | if not match then 20 | --try long version of string func 21 | _, e, match = sfind(str, "^%[([%w_]*)%]", pos) 22 | end 23 | if match then 24 | local replacement = replacement_data and replacement_data[match] 25 | if not replacement then 26 | return "", e+1 27 | else 28 | return tostring(replacement), e+1 29 | end 30 | end 31 | 32 | error("invalid string replacement ".. str:sub(pos, pos + 10)) 33 | end --}}} 34 | 35 | local find_next_special do --{{{ 36 | local pattern_memo = {} 37 | 38 | function find_next_special(str, pos, pat) 39 | local findpattern = pattern_memo[pat] 40 | if not findpattern then 41 | local pre, post = "^([^", "]*)" 42 | local ti = table.insert 43 | local pattern = {pre} 44 | 45 | --escapes non-alphanumeric characters 46 | --alphanumeric are turned into character classes 47 | --undefined if a character class doesn't exist for alphanumeric 48 | for i = 1, #pat do 49 | ti(pattern, "%") 50 | ti(pattern, pat:sub(i, i)) 51 | end 52 | ti(pattern, post) 53 | 54 | findpattern = table.concat(pattern) 55 | pattern_memo[pat] = findpattern 56 | end 57 | local s, e, match = sfind(str, findpattern, pos) 58 | return match, e+1 59 | end end --}}} 60 | 61 | local function Stack() --{{{ 62 | return setmetatable({poptable = {}, count = 0}, {__index = { 63 | push = function(self, ...) 64 | for i = 1, select("#", ...) do 65 | local item = select(i, ...) 66 | if item then 67 | local c = self.count + 1 68 | self.count = c 69 | self[c] = item 70 | end 71 | end 72 | end, 73 | pop = function(self, num) 74 | if not num or num == 1 then 75 | local c = self.count - 1 76 | if c < 0 then 77 | return 78 | end 79 | self.count = c 80 | return self[c+1] 81 | elseif type(num) == "number" then 82 | local c = self.count 83 | local t = self.poptable --reusing to save allocations 84 | local popc = 0 85 | for i = 1, num do 86 | c = c - 1 87 | if c < 0 then 88 | break 89 | end 90 | t[i] = self[c+1] 91 | popc = i 92 | end 93 | return unpack(t, 1, popc) 94 | end 95 | end, 96 | }}) 97 | end --}}} 98 | 99 | local function escape_char(specials, str, pos) 100 | local sc = ssub(str, pos, pos) 101 | return specials[sc] or "", pos+1 102 | end 103 | 104 | local special_base = { 105 | ["n"] = "\n", 106 | ["r"] = "\r", 107 | ["t"] = " ", 108 | ["\\"] = "\\\\", 109 | 110 | [" "] = " ", 111 | ["$"] = "$", 112 | ["\""] = "\"", 113 | ["'"] = "'", 114 | } 115 | 116 | local special_double = { 117 | ["n"] = "\n", 118 | ["r"] = "\r", 119 | ["t"] = " ", 120 | ["\\"] = "\\\\", 121 | 122 | ["\""] = "\"", 123 | ["$"] = "$", 124 | } 125 | 126 | 127 | local function sap(_, str, replacement_data, pos) 128 | if type(str) ~= "string" then 129 | error(sfmt("arg1 must be a string, not a \"%s\"", type(str)), 2) 130 | end 131 | local _ _, pos = string.find(str, "^%s*", pos or 1) 132 | pos = pos + 1 133 | if pos > #str then return end 134 | 135 | local parsed = Stack() 136 | while true do 137 | if pos > #str then 138 | if parsed.count == 0 then pos = nil end 139 | break 140 | end 141 | local pstr pstr, pos = find_next_special(str, pos, "\\$'\"s") 142 | parsed:push(pstr) pstr = nil 143 | --print(table.concat(parsed)) 144 | 145 | if sfind(str, "^\\", pos) then 146 | pstr, pos = escape_char(special_base, str, pos+1) 147 | elseif sfind(str, "^%$", pos) then 148 | pstr, pos = replace_variable(str, pos+1, replacement_data) 149 | elseif sfind(str, "^'", pos) then 150 | pstr, pos = find_next_special(str, pos+1, "'") 151 | pos = pos + 1 --skip ending single quote 152 | elseif sfind(str, "^\"", pos) then 153 | -- Double Quote ---------------------------------------------------------------- 154 | pos = pos + 1 155 | while true do 156 | local pstr pstr, pos = find_next_special(str, pos, "\\$\"") 157 | parsed:push(pstr) pstr = nil 158 | 159 | if sfind(str, "^\\", pos) then 160 | pstr, pos = escape_char(special_double, str, pos+1) 161 | elseif sfind(str, "^%$", pos) then 162 | pstr, pos = replace_variable(str, pos+1, replacement_data) 163 | elseif sfind(str, "^\"", pos) then 164 | pos = pos + 1 165 | break 166 | end 167 | parsed:push(pstr) 168 | end 169 | -------------------------------------------------------------------------------- 170 | elseif sfind(str, "^%s", pos) then 171 | pos = pos + 1 172 | break 173 | end 174 | parsed:push(pstr) 175 | end 176 | if parsed.count == 0 then 177 | return pos 178 | else 179 | return pos, table.concat(parsed, "", 1, parsed.count) 180 | end 181 | end 182 | 183 | return setmetatable({}, { 184 | __call = sap, 185 | __index = { 186 | loop = function(str, replacement_data) 187 | return function(str, pos) 188 | return sap(nil, str, replacement_data, pos) 189 | end, str, 1 190 | end, 191 | }, 192 | }) 193 | -------------------------------------------------------------------------------- /data/strict.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2020 -- Scott Smith -- 2 | -- MIT License 3 | -------------------------------------------------------------------------------- 4 | -- strict.lua 5 | -- Prevents all variables in a table from being set without the use of a 6 | -- setter (a special variable of the table). If a variable has not been 7 | -- intialized, it will throw an error when accessed. 8 | -- 9 | -- Will not work with tables that use metatables. 10 | -- 11 | -- Usage: 12 | -- require"strict"("export", _G) 13 | -- --newglobal = "this would throw an error" 14 | -- --table = "as will this" 15 | -- 16 | -- export. newglobal = "I've just initialized and set a global variable!" 17 | -- export. table = "this works just fine" 18 | -- 19 | -- --Lock any table 20 | -- local newtable = {} 21 | -- require"strict"("set", newtable) 22 | -- newtable.error = 1 --this will fail 23 | -- newtable.set.error = 1 --but this will not 24 | -- 25 | -- print(newtable.error) --prints: 1 26 | -------------------------------------------------------------------------------- 27 | local getmetatable, setmetatable = getmetatable, setmetatable 28 | local error, debug = error, debug 29 | local next, strfmt, type = next, string.format, type 30 | 31 | local _VERSION = "Strict 0.9.0" 32 | 33 | local function strict(settername, tbl) 34 | if type(settername) ~= "string" then 35 | error("The variable setter (arg1) must be a string.", 2) 36 | end 37 | if type(tbl) ~= "table" then 38 | error("No table (arg2) to lock.", 2) 39 | end 40 | if getmetatable(tbl) then 41 | error("No support for metatables. Cannot lock table.", 2) 42 | end 43 | if tbl[settername] ~= nil then 44 | error(strfmt( 45 | "Strict setter \"%s\" has previously been set!", settername), 2) 46 | end 47 | 48 | local index = {} 49 | local index_si = {} --index's strict index (if it's set, true) 50 | for k,v in next, tbl do 51 | index_si[k] = true 52 | index[k] = v 53 | tbl[k] = nil 54 | end 55 | 56 | index_si[settername] = true 57 | index[settername] = setmetatable({},{ 58 | __call = function(_, cmd, key) 59 | if cmd == "version" then return _VERSION end 60 | if cmd == "check" and key then return index_si[key], index[key] end 61 | if cmd == "clear" and key == "all" then 62 | local setter = index[settername] 63 | index_si = {[settername] = true} 64 | index = {[settername] = setter} 65 | return true 66 | end 67 | end, 68 | __newindex = function(_, key, value) 69 | if key == settername then 70 | error("Cannot change strict setter variable once set", 2) 71 | end 72 | if value == nil then 73 | index_si[key] = nil 74 | else 75 | index_si[key] = true 76 | end 77 | index[key] = value 78 | end, 79 | __index = function(_, key) 80 | error(strfmt( 81 | "What are you trying to do? \"%s\" is not getter.", 82 | settername), 2) 83 | end, 84 | }) 85 | 86 | local meta = { 87 | __newindex = function(_, key, value) 88 | error(debug.traceback(strfmt( 89 | "Table locked! Must set variable \"%s\" with setter: \"%s\"", 90 | key, settername) , 2), 2) 91 | end, 92 | __index = function (_, k) 93 | if index_si[k] then 94 | return index[k] 95 | end 96 | error(strfmt("The strict variable \"%s\" was not initialized!", k), 2) 97 | end 98 | } 99 | setmetatable(tbl, meta) 100 | 101 | return index[settername] 102 | end 103 | 104 | return setmetatable({}, { 105 | __call = function(_, sname, tbl, set) return strict(sname, tbl, set) end, 106 | __index = { _VERSION = _VERSION } 107 | }) 108 | -------------------------------------------------------------------------------- /data/sysconsole.lua: -------------------------------------------------------------------------------- 1 | local VERSION = "alpha-0" 2 | 3 | local tinsert, tconcat, tremove = table.insert, table.concat, table.remove 4 | local ssub, sfind = string.sub, string.find 5 | 6 | local floor = math.floor 7 | 8 | local cmd = require "commands" 9 | 10 | local lg = love.graphics 11 | 12 | 13 | -- {{{ LPEG Functions 14 | --local lP, lS, lR, 15 | -- lC, lCt, lCs, lCp, 16 | -- lmatch, lV 17 | -- = lpeg.P, lpeg.S, lpeg.R, 18 | -- lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Cp, 19 | -- lpeg.match, lpeg.V 20 | 21 | --local getescape = setmetatable({ 22 | -- t = "\t", 23 | -- v = "\v", 24 | -- r = "\r", 25 | -- n = "\n", 26 | -- e = "\27", --ESC (escape) 27 | --}, 28 | --{__call = function (self, esc_char) 29 | -- return self[esc_char] or false 30 | --end}) 31 | 32 | --local backslash = lP"\\" 33 | --local escseq = backslash * lC(lP(1)) / getescape 34 | --local newline = lP"\r\n" + lS"\r\n" 35 | --local htab = lP"\t" 36 | --local vtab = lP"\v" 37 | --local escape = lP"\27" 38 | 39 | --local escbyte = escseq + newline + htab + vtab + escape 40 | 41 | --uses lpeg 42 | --local function format(s, linewidth) 43 | --local function getframebuffer(str, height) 44 | --local function getcursoronstring(str) 45 | 46 | local function format(s, linewidth) 47 | local I = lCp() 48 | local nl = lCs(newline/"") 49 | local line = lC((1 - nl)^-linewidth) * newline^-1 50 | local linenl = (nl + line) * I 51 | assert(s, "all commands must return a string (sysconsole)") 52 | local slen = #s 53 | 54 | local rt = {} 55 | 56 | local e = 1 57 | local m 58 | while e and e <= slen do 59 | m, e = lmatch(linenl, s, e) 60 | if m then 61 | tinsert(rt, m) 62 | end 63 | end 64 | 65 | local tlen = #rt 66 | return tconcat(rt, "\n") 67 | end 68 | 69 | 70 | --Basically at most height lines of formatted output. 71 | --all output should've already been formatted 72 | 73 | local function getframebuffer(str, height) 74 | local line = (1 - newline)^0 * newline^-1 * lCp() 75 | 76 | local col_count = {1} 77 | local cclen = 0 78 | local slen = #str 79 | local e = 1 80 | local m 81 | while e and e <= slen do 82 | e = lmatch(line, str, e) 83 | if e then 84 | cclen = cclen + 1 85 | col_count[cclen] = e 86 | end 87 | end 88 | local width = col_count[cclen] - col_count[cclen-1] + 1 89 | if cclen > height then 90 | local init = cclen - height 91 | return ssub(str, col_count[init]), height, width 92 | end 93 | return str, cclen, width 94 | end 95 | 96 | local function getcursoronstring(str) 97 | local slen = #str 98 | local e = 1 99 | local m 100 | local I = lCp() 101 | local line = (1 - newline)^0 * newline^-1 * I 102 | local lastline = lP(1) - (newline^-1 * I * (1 - newline)^0 * -1 ) 103 | --x and y start at one. we always 104 | local x, y = 1, 1 105 | while true do 106 | e = lmatch(line, str, e) 107 | if e and e < slen then 108 | y = y + 1 109 | else 110 | break 111 | end 112 | end 113 | local c = lmatch(lastline^0, str) 114 | x = x + slen - c 115 | return x, y 116 | end 117 | 118 | --}}} 119 | 120 | --{{{ Functions; addBufferLine 121 | 122 | local function addBufferLine(buffer, str) 123 | buffer.size = buffer.size + 1 124 | if buffer.size > buffer.maxsize then 125 | buffer.size = buffer.maxsize 126 | tremove(buffer, 1) --slow operation, but it might not matter 127 | end 128 | buffer[buffer.size] = str 129 | if buffer.selected_row then 130 | buffer.selected_row = buffer.size + 1 131 | end 132 | end 133 | 134 | --}}} 135 | 136 | local function sysconsole(entity, dt) 137 | local ecomp = entity.component 138 | local console = ecomp.console 139 | if not console then return end 140 | 141 | --{{{ Initialize console if not done already 142 | if not console.version then 143 | --initialize(console) 144 | local ec = console 145 | do --load font 146 | local font = ec.font or lg.getFont() 147 | local cellheight = font:getHeight() 148 | local cellwidth 149 | if font:getWidth(".") == font:getWidth("W") then 150 | cellwidth = font:getWidth(".") 151 | else 152 | print("WARNING: Non-monospace fonts are not supported.") 153 | local lm, lw = font:getWidth("M"), font:getWidth("W") 154 | cellwidth = lm > lw and lm or lw 155 | end 156 | ec.font, ec.cellwidth, ec.cellheight = 157 | font, cellwidth, cellheight 158 | end 159 | 160 | --magic number .5 is to reduce the 2x scale. this will break! 161 | --However, currently the console is always rendered at 2x scale 162 | do --resize terminal 163 | local w, h = lg.getDimensions() 164 | ec.termwidth = floor(w * 0.5 / ec.cellwidth) 165 | ec.termheight = floor(h * 0.5 / ec.cellheight) 166 | end 167 | 168 | ec.buffer = {"Connected...", maxsize = 1000} 169 | --buffer maxsize must be at least termheight tall 170 | if ec.buffer.maxsize < ec.termheight then 171 | ec.buffer.maxsize = ec.termheight 172 | end 173 | ec.buffer.size = #ec.buffer 174 | 175 | ec.ibuffer = {selected_row = 1, size = 0, maxsize = 1000} 176 | ec.rawinput = {} --gets data from input system 177 | ec.version = VERSION 178 | ec.output = "" 179 | ec.input = "" 180 | ec.input_cursor = 1 181 | ec.cursor_x = 1 182 | ec.cursor_y = 1 183 | ec.prompt = "[ Prompt 9000 ] " 184 | end --}}} 185 | 186 | console.output = "" 187 | if ecomp._turnon then 188 | ecomp._turnon = nil --clear the variable (message) 189 | console.on = not console.on 190 | end 191 | if not console.on then return end 192 | 193 | --[ Console is on ]-- 194 | 195 | --prerender output 196 | --draw on last line 197 | local output = {} 198 | local bufferstart = console.buffer.size - console.termheight 199 | if bufferstart < 1 then bufferstart = 1 end 200 | for i = bufferstart, console.buffer.size do 201 | tinsert(output, format(console.buffer[i], console.termwidth)) 202 | tinsert(output, "\n") 203 | end 204 | 205 | tinsert(output, console.prompt) 206 | --finds the cursor. Seems easy to break. 207 | console.cursor_x, console.cursor_y = getcursoronstring(tconcat(output)) 208 | 209 | 210 | local input = console.input 211 | local icursor = console.input_cursor 212 | 213 | for i = 1, #console.rawinput do local cin = console.rawinput[i] 214 | if type(cin) == "string" then 215 | if #cin == 1 then 216 | local tail = ssub(input, icursor) 217 | input = tconcat{ssub(input, 1, icursor-1), cin, tail} 218 | icursor = icursor + 1 219 | elseif cin == "up" then 220 | local ib = console.ibuffer 221 | local selrow = ib.selected_row - 1 222 | if selrow < 1 then 223 | selrow = 1 224 | end 225 | input = ib[selrow] or "" 226 | ib.selected_row = selrow 227 | icursor = #input + 1 228 | elseif cin == "down" then 229 | local ib = console.ibuffer 230 | local selrow = ib.selected_row + 1 231 | if selrow > ib.size then 232 | selrow = ib.size + 1 233 | input = "" 234 | else 235 | input = ib[selrow] 236 | end 237 | ib.selected_row = selrow 238 | icursor = #input + 1 239 | elseif cin == "left" then 240 | icursor = icursor - 1 241 | elseif cin == "right" then 242 | icursor = icursor + 1 243 | elseif cin == "home" then 244 | icursor = 1 245 | elseif cin == "end" then 246 | icursor = #input + 1 247 | end 248 | elseif type(cin) == "number" then 249 | local char 250 | if cin == 0x08 then --backspace 251 | icursor = icursor - 1 252 | if input then 253 | local tail = ssub(input, icursor+1) 254 | input = ssub(input, 1, icursor-1)..tail 255 | end 256 | elseif cin == 0x0a then --newline 257 | --need to process commands here 258 | --The buffer size should be updated when the buffer is, 259 | --with a function 260 | addBufferLine(console.buffer, console.prompt..input) 261 | addBufferLine(console.ibuffer, input) 262 | addBufferLine(console.buffer, cmd.execute(input)) 263 | input = "" 264 | icursor = 1 265 | elseif cin == 0x7f then --delete 266 | --cursor stays in the same position 267 | if input then 268 | local tail = ssub(input, icursor+1) 269 | input = ssub(input, 1, icursor-1)..tail 270 | end 271 | end 272 | if char then 273 | end 274 | end end 275 | 276 | if icursor < 1 then icursor = 1 end 277 | local maxlen = #input + 1 278 | if icursor > maxlen then icursor = maxlen end 279 | console.input_cursor = icursor 280 | 281 | console.rawinput = {} 282 | local inputlen = #input 283 | console.input = input 284 | 285 | tinsert(output, format(input, console.termwidth - console.cursor_x)) 286 | do -- getframebuffer 287 | --local function getframebuffer(str, height) 288 | --console.output, console.cursor_y, console.cursor_x = 289 | -- getframebuffer(tconcat(output), console.termheight) 290 | --USES LPEG 291 | local str = tconcat(output) 292 | local height = console.termheight 293 | 294 | local line = (1 - newline)^0 * newline^-1 * lCp() 295 | 296 | local col_count = {1} 297 | local cclen = 0 298 | local slen = #str 299 | local e = 1 300 | local m 301 | while e and e <= slen do 302 | e = lmatch(line, str, e) 303 | if e then 304 | cclen = cclen + 1 305 | col_count[cclen] = e 306 | end 307 | end 308 | local width = col_count[cclen] - col_count[cclen-1] + 1 309 | if cclen > height then 310 | local init = cclen - height 311 | return ssub(str, col_count[init]), height, width 312 | end 313 | 314 | console.output, console.cursor_y, console.cursor_x = 315 | str, cclen, width 316 | end 317 | 318 | local curdiff = icursor-1 - inputlen 319 | local cx = console.cursor_x + curdiff 320 | --This doesn't work for more than two lines 321 | if cx < 1 then 322 | console.cursor_y = console.cursor_y - 1 323 | cx = cx + console.termwidth - 1 324 | end 325 | console.cursor_x = cx 326 | end 327 | 328 | return sysconsole 329 | -------------------------------------------------------------------------------- /data/sysinput.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2020 -- Scott Smith -- 2 | 3 | local keys2 = keys2 4 | local keys = keys2.keys 5 | 6 | local vec = require "Vector" 7 | 8 | local input = {_VERSION = "SysInput 0.5.0", 9 | count = 0, sidx = 0, bind = {}, 10 | edit_mode_enabled = false, 11 | text = "", 12 | edit = {count = 0}, 13 | } 14 | --{{{ setting up hal.input metatable 15 | local hal = hal 16 | local input_key_lookup = { 17 | mouse = true, gpaxis = true, _VERSION = true, initialize = true, 18 | edit_mode = true, focus_clickthrough = true, text = true, 19 | processed = true, edit = true, 20 | } 21 | hal.input = setmetatable({}, { 22 | __index = function(self, key) 23 | if input_key_lookup[key] then 24 | --if key == "mouse" or 25 | -- key == "gpaxis" or 26 | -- key == "_VERSION" or 27 | -- key == "initialize" or 28 | -- key == "edit_mode" or 29 | -- key == "focus_clickthrough" or 30 | -- key == "text" 31 | --then 32 | return input[key] 33 | end 34 | for i = 0, input.count-2, 2 do 35 | local ikey = input[i+1] 36 | if ikey == key then 37 | return input[i+2] 38 | end 39 | end 40 | end, 41 | __newindex = function(self, key, value) 42 | if key == "text" then 43 | input[key] = value 44 | return 45 | elseif key == "processed" then 46 | input[key] = value 47 | return 48 | end 49 | error("cannot set input variables", 2) 50 | end, 51 | })--}}} 52 | 53 | local setmetatable, type, rawget 54 | = setmetatable, type, rawget 55 | local sfmt = string.format 56 | 57 | do --{{{ MOUSE FOCUS CLICKTHROUGH 58 | --Thanks to slime and zorg on the LÖVE discord server 59 | local ffi = require("ffi") 60 | local sdl = ffi.os == "Windows" and ffi.load("SDL2") or ffi.C 61 | 62 | if not hal_defined.sdl_sethint then 63 | hal_defined.sdl_sethint = true 64 | ffi.cdef[[ 65 | typedef enum SDL_bool { 66 | SDL_FALSE = 0, 67 | SDL_TRUE = 1 68 | } SDL_bool; 69 | 70 | SDL_bool SDL_SetHint(const char *name, 71 | const char *value); 72 | ]] 73 | end 74 | 75 | --sdl.SDL_SetHint("SDL_MOUSE_FOCUS_CLICKTHROUGH", "1") 76 | function input.focus_clickthrough(enable) 77 | local value = "0" 78 | if enable then 79 | value = "1" 80 | end 81 | local ret = sdl.SDL_SetHint("SDL_MOUSE_FOCUS_CLICKTHROUGH", value) 82 | return (ret == sdl.SDL_TRUE) 83 | end 84 | end--}}} 85 | -------------------------------------------------------------------------------- 86 | -- KEYBINDING 87 | -- 88 | -- This should probably be separated into another file at some point 89 | -------------------------------------------------------------------------------- 90 | local keybind = { 91 | global = { --{{{ 92 | --DEBUG KEYS 93 | --["end"] = function()error("HALT_THE_GAME_PLEASE")end, 94 | --["home"] = "debug_menu", 95 | 96 | --UNKNOWN KEYS 97 | ["space"] = "jump", 98 | ["return"] = "pause", 99 | ["lshift"] = "shift", 100 | ["rshift"] = "shift", 101 | ["delete"] = "delete", 102 | 103 | 104 | 105 | --[[{{{ example usage of mouse_click in game: 106 | if input.mouse_click then 107 | print("mouse_click is down") 108 | elseif input.mouse_click == "pressed" then 109 | print("mouse_click has just been pressed") 110 | elseif not input.mouse_click then 111 | print("mouse_click has been or is released") 112 | end 113 | --}}}]] 114 | ["pressed1"] = {"pressed", "mouse_click"}, 115 | ["released1"] = {"released", "mouse_click"}, 116 | ["pressed2"] = {"pressed", "mouse_menu"}, 117 | ["released2"] = {"released", "mouse_menu"}, 118 | ["pressed3"] = {"pressed", "mouse_middle"}, 119 | ["released3"] = {"released", "mouse_middle"}, 120 | }, --}}} 121 | } 122 | 123 | local words_to_symbols = { --{{{ 124 | zero = "0", 125 | one = "1", 126 | two = "2", 127 | three = "3", 128 | four = "4", 129 | five = "5", 130 | six = "6", 131 | seven = "7", 132 | eight = "8", 133 | nine = "9", 134 | exclamation = "!", 135 | double_quote = "\"", 136 | hash = "#", 137 | dollar = "$", 138 | ampersand = "&", 139 | single_quote = "'", 140 | left_parenthesis = "(", 141 | right_parenthesis = ")", 142 | astrisk = "*", 143 | plus = "+", 144 | comma = ",", 145 | minus = "-", 146 | period = ".", 147 | slash = "/", 148 | colon = ":", 149 | semicolon = ";", 150 | less_than = "<", 151 | equal = "=", 152 | greater_than = ">", 153 | question = "?", 154 | at = "@", 155 | left_square_bracket = "[", 156 | backslash = "\\", 157 | right_square_bracket = "]", 158 | caret = "^", 159 | underscore = "_", 160 | grave_accent = "`", 161 | 162 | kp_decimal = "kp.", 163 | kp_comma = "kp,", 164 | kp_divide = "kp/", 165 | kp_multiply = "kp*", 166 | kp_subtract = "kp-", 167 | kp_add = "kp+", 168 | kp_equals = "kp=", 169 | kp_enter = "kpenter", 170 | } --}}} 171 | 172 | function hal.input_load_bindings(bind) 173 | local warning = false 174 | local loading_str = "Loading Key Bindings..." 175 | printf(loading_str) 176 | input.bind = {global = {}} 177 | local ib = input.bind 178 | for k,v in next, bind.global do 179 | local symbol = words_to_symbols[k] 180 | if symbol then 181 | bind.global[k] = nil 182 | bind.global[symbol] = v 183 | end 184 | end 185 | for k,v in next, keybind.global do 186 | if bind.global and bind.global[k] then 187 | local default_global = v 188 | local new_global = bind.global[k] 189 | if type(default_global) == "table" then 190 | default_global = sfmt("{%s, %s}", v[1], v[2]) 191 | end 192 | if type(new_global) == "table" then 193 | new_global = sfmt("{%s, %s}", new_global[1], new_global[2]) 194 | end 195 | if default_global ~= new_global then 196 | printf("\nWARNING: overwriting global keybind '%s' from '%s' to '%s'", k, default_global, new_global) 197 | warning = true 198 | end 199 | else 200 | ib.global[k] = v 201 | end 202 | end 203 | for ksec,vtab in next, bind do 204 | local ibk = ib[ksec] or {} 205 | ib[ksec] = ibk 206 | for k,v in next, vtab do 207 | ibk[k] = v 208 | end 209 | end 210 | if warning then 211 | printf("\n%s", loading_str) 212 | end 213 | printf("done\n") 214 | end 215 | 216 | --keybindings 217 | local function modifier(mtype) 218 | --need to be able to bind everything. 219 | --in windows lctrl is correctly reported (as capslock on my system) 220 | --in linux lctrl is still reported as the key on the keyboard 221 | --so in linux I need to use keys.capslock rather than keys.lctrl 222 | if mtype == "ctrl" and (keys.lctrl or keys.rctrl) then return true 223 | elseif mtype == "alt" and (keys.lalt or keys.ralt) then return true 224 | elseif mtype == "shift" and (keys.lshift or keys.rshift) then return true 225 | --annoyingly, gui is used for command in apple (normally treated as CTRL) 226 | --is treated as the Windows key in windows (probably meta key in Linux) 227 | elseif mtype == "gui" and (keys.lgui or keys.rgui) then return true 228 | end 229 | end 230 | 231 | do --{{{ ( keybind.global; mouse, wheel, and gamepad functions ) 232 | local svec do 233 | local ffi = require "ffi" 234 | if not hal_defined.twovector2 then 235 | hal_defined.twovector2 = true 236 | ffi.cdef[[ 237 | struct twovector2 { 238 | struct vector2 absolute; 239 | struct vector2 relative; 240 | struct vector2 wheel; 241 | };]] 242 | end 243 | svec = ffi.new(ffi.typeof("struct twovector2")) 244 | end 245 | input["mouse"] = svec 246 | 247 | local axisvec do 248 | local ffi = require "ffi" 249 | if not hal_defined.axisvec then 250 | hal_defined.axisvec = true 251 | ffi.cdef[[ 252 | struct axisvec { 253 | struct vector2 left_stick; 254 | struct vector2 right_stick; 255 | double left_trigger, right_trigger; 256 | };]] 257 | end 258 | axisvec = ffi.new(ffi.typeof("struct axisvec")) 259 | end 260 | input["gpaxis"] = axisvec 261 | 262 | function keybind.global.moved(dt, x, y, dx, dy) 263 | svec.absolute.x = x or 0 264 | svec.absolute.y = y or 0 265 | svec.relative.x = dx or 0 266 | svec.relative.y = dy or 0 267 | end 268 | 269 | function keybind.global.wheel(dt, x, y) 270 | svec.wheel.x = svec.wheel.x + x 271 | svec.wheel.y = svec.wheel.y + y 272 | end 273 | 274 | --TODO:how can I turn input.gpaxis.left_stick.x to something more pleasing 275 | --like in keybind.cfg (gp1_leftx = move_x) 276 | function keybind.global.axis(dt, id, axis, value) 277 | if axis == "gp1_leftx" then 278 | axisvec.left_stick.x = value 279 | elseif axis == "gp1_lefty" then 280 | axisvec.left_stick.y = value 281 | elseif axis == "gp1_rightx" then 282 | axisvec.right_stick.x = value 283 | elseif axis == "gp1_righty" then 284 | axisvec.right_stick.y = value 285 | elseif axis == "gp1_triggerleft" then 286 | axisvec.left_trigger = value 287 | elseif axis == "gp1_triggerright" then 288 | axisvec.right_trigger = value 289 | end 290 | end 291 | end 292 | --}}} 293 | 294 | function input.edit_mode(enabled) 295 | local enabled_type = type(enabled) 296 | if enabled_type == "nil" then 297 | return input.edit_mode_enabled 298 | elseif enabled_type == "boolean" then 299 | love.keyboard.setKeyRepeat(enabled) 300 | input.edit_mode_enabled = enabled 301 | return enabled 302 | else 303 | error("invalid input.edit_mode argument: ".. enabled_type) 304 | end 305 | end 306 | 307 | local function new_keypress(key) 308 | local c = input.count 309 | local si = input.sidx --start index 310 | local ptr 311 | if input.edit_mode_enabled then 312 | for i = 0, c, 2 do 313 | if input[i+1] == key then 314 | ptr = i 315 | end 316 | end 317 | end 318 | if not ptr then 319 | for i = si, c, 2 do 320 | if not input[i+1] then 321 | ptr = i 322 | si = i + 2 323 | input.sidx = si 324 | if i == c then input.count = si end 325 | break 326 | end 327 | end 328 | end 329 | 330 | input[ptr+1] = key 331 | input[ptr+2] = "pressed" 332 | end 333 | 334 | local function new_keyrelease(key) 335 | for i = 0, input.count-2, 2 do 336 | if input[i+1] == key then 337 | input[i+1] = false 338 | input[i+2] = false 339 | if input.sidx > i then input.sidx = i end 340 | break 341 | end 342 | end 343 | end 344 | 345 | local function new_keydown() 346 | local last_idx = -2 347 | for i = 0, input.count-2, 2 do 348 | if input[i+2] then 349 | last_idx = i 350 | input[i+2] = "down" 351 | end 352 | end 353 | --if input[i+2] is always false, count will be 0 354 | input.count = last_idx + 2 355 | end 356 | 357 | local function new_clearkeys() 358 | input.edit_mode(false) 359 | for i = 0, input.count-2, 2 do 360 | input[i+1], input[i+2] = false, false 361 | end 362 | input.count = 0 363 | input.sidx = 0 364 | end 365 | input.initialize = new_clearkeys 366 | 367 | --{{{-[[ event handlers (keypress, keyrelease, mouseevent) ]]------------------- 368 | local keydown = {count = 0} --happens as long as the key is pressed down 369 | local mousedown = {count = 0} 370 | local inputdown = {count = 0, startidx = 0} 371 | -- inputdown[c+1] --> keybinding name 372 | -- inputdown[c+2] --> downtime 373 | 374 | local keypress = setmetatable({}, {__index = function(self, key) 375 | new_keypress(key) 376 | end}) 377 | local keyrelease = setmetatable({}, {__index = function(self, key) 378 | new_keyrelease(key) 379 | end}) 380 | 381 | local mouseevent = setmetatable({}, {__index = function(self, key) 382 | local ktype = type(key) 383 | if ktype == "function" then 384 | return key 385 | elseif ktype == "table" then 386 | local ev, ktype = key[1], key[2] 387 | if ev == "pressed" then 388 | new_keypress(ktype) 389 | elseif ev == "released" then 390 | new_keyrelease(ktype) 391 | end 392 | return 393 | end 394 | 395 | error(sfmt("unsupported mouseevent key type: %s; %s, %s", 396 | ktype, key[1], key[2] 397 | )) 398 | end}) 399 | --}}} 400 | 401 | local function keyfunc(kev, ename, key, dt, ...) 402 | local kb = input.bind[ename][key] 403 | if not kb then return end 404 | if type(kb) == "function" then 405 | return kb(dt, ...) 406 | end 407 | local f = kev[kb] --kev are metatables which sets input 408 | if type(f) == "function" then f(dt, ...) end 409 | end 410 | 411 | local function system(dt) 412 | local edit_mode = input.edit_mode_enabled 413 | local in_bind = input.bind 414 | local input = hal.input 415 | 416 | local ename = "global" 417 | 418 | input.mouse.wheel.x, input.mouse.wheel.y = 0, 0 419 | new_keydown() 420 | 421 | 422 | 423 | do --{{{ process key/mouse events 424 | local k2t, kev = keys2.pressed, keypress 425 | for i = 1, k2t.count do 426 | keyfunc(kev, ename, k2t[i], dt) 427 | end 428 | 429 | k2t, kev = keys2.released, keyrelease 430 | for i = 1, k2t.count do 431 | keyfunc(kev, ename, k2t[i], dt) 432 | end 433 | 434 | k2t, kev = keys2.mouseevent, mouseevent 435 | local i = 0 436 | while i < k2t.count do 437 | local size 438 | local etype, x, y, button_dx, dy 439 | = k2t[i+1], k2t[i+2], k2t[i+3], k2t[i+4] --nil 440 | if etype == "moved" then 441 | size = k2t.size[etype] 442 | dy = k2t[i+size] 443 | elseif etype == "wheel" then 444 | button_dx = nil 445 | size = k2t.size[etype] 446 | else -- etype = pressed or released 447 | -- etype1 | Left Mouse Button. 448 | -- etype3 | Middle Mouse Button. 449 | -- etype2 | Right Mouse Button. 450 | -- etypen | etc. 451 | size = k2t.size.button 452 | etype = etype..button_dx 453 | end 454 | keyfunc(kev, ename, etype, dt, x, y, button_dx, dy) 455 | i = i + size 456 | end 457 | 458 | k2t, kev = keys2.gamepad, nil 459 | local i = 0 460 | while i < k2t.count do 461 | local etype, id, button_axis, value 462 | = k2t[i+1], k2t[i+2], k2t[i+3], k2t[i+4] 463 | if etype == "pressed" then 464 | keyfunc(keypress, ename, button_axis, dt) 465 | elseif etype == "released" then 466 | keyfunc(keyrelease, ename, button_axis, dt) 467 | elseif etype == "axis" then 468 | keyfunc(nil, ename, etype, dt, id, button_axis, value) 469 | end 470 | i = i + k2t.size[etype] 471 | end 472 | 473 | k2t, kev = keys2.touchevent, nil 474 | local i = 0 475 | while i < k2t.count do 476 | local etype, id, x, y, dx, dy, pressure 477 | = k2t[i+1], k2t[i+2], k2t[i+3], k2t[i+4]--nil,nil,nil 478 | if etype == "moved" then 479 | dx = k2t[i+5] 480 | dy = k2t[i+6] 481 | pressure = k2t[i+7] 482 | else --etype == "pressed" or "released" 483 | pressure = k2t[i+5] 484 | end 485 | --TODO:Handle touchevents in sysinput 486 | i = i + k2t.size[etype] 487 | end 488 | end --}}} 489 | 490 | ---[[ not used 491 | --this is for text input 492 | if edit_mode then 493 | input.text = "" 494 | local k2txt = keys2.text 495 | if k2txt.count > 0 then 496 | local textinput = {} 497 | for i = 1, k2txt.count do 498 | textinput[i] = k2txt[i] 499 | end 500 | input.text = table.concat(textinput) 501 | end 502 | local kall = keys2.all 503 | local ec = 0 504 | for i = 1, kall.count, 2 do 505 | local ev = kall[i] 506 | local key = kall[i+1] 507 | if ev == "pressed" then 508 | local kb = in_bind[ename][key] 509 | if kb then 510 | input.edit[ec+1] = ev 511 | input.edit[ec+2] = kb 512 | ec = ec + 2 513 | end 514 | elseif ev == "text" then 515 | if input.edit[ec-1] == "text" then 516 | ec = ec - 2 517 | key = input.edit[ec+2] .. key 518 | end 519 | input.edit[ec+1] = ev 520 | input.edit[ec+2] = key 521 | ec = ec + 2 522 | end 523 | end 524 | input.edit.count = ec 525 | 526 | end 527 | --]] 528 | end 529 | 530 | return system 531 | -------------------------------------------------------------------------------- /data/utils/README.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Utils Directory 5 | 6 | 7 | 8 | 9 | What makes a file a utility? 10 | 11 | I define a utility relatively generic one off functions as functions 12 | that are small in scope and only do one very specific thing. It has no 13 | state to maintain and no side effects. These are generally small files 14 | consisting of less than 100 lines of code—100 lines of code is only a 15 | guideline, some utilties could be much larger. 16 | 17 | To expand on the idea, utilties have pretty much one way to impliment 18 | them with few if any design decisions needing to be put into them. These 19 | would be perfect files to include as a standard library in almost any 20 | language. 21 | 22 | Also, utils generally can't logically be included in any other library 23 | very well. Again, they're pretty simple functions but need to be written 24 | down somewhere. 25 | 26 | 27 | Currently, in this engine (CODENAME: Coffee Hour) a few of the files 28 | located in the immediate parent directory could be considered for 29 | inclusion in the utils directory. They'd have uses outside of a game 30 | engine (and indeed I have used them in other programs) and are pretty 31 | generic. Almost every program could include these examples and find some 32 | sort of use for them: 33 | 34 | * sap.lua -- string argument parser 35 | * json.lua -- json decoder/encoder 36 | * ini.lua -- ini decoder (encoder currently broken) 37 | * strict.lua -- strict lua table values 38 | 39 | Some other files that could be included in utils but their use cases 40 | are much less generic than the previous examples and are less likely 41 | to be needed in any random program would include: 42 | 43 | * OrderedSet.lua -- sorting library 44 | * PriorityQueue.lua -- sorting library 45 | * Vector.lua -- Vector math datatype 46 | 47 | 48 | The rest of the files would not be useful outside the context of LÖVE or 49 | other software besides this engine: 50 | 51 | * bump.lua -- collision library 52 | * keys2.lua -- input event handling 53 | * sysinput.lua -- keyconf and input system 54 | * sysconsole.lua -- broken console library (why does it still exist?) 55 | * main2.lua -- main entry point for the engine 56 | * Hitbox.lua -- unused and should be removed, Hitbox datatype 57 | -------------------------------------------------------------------------------- /data/utils/args.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Argument types 4 | short options: -s, -d, -r 5 | long options: --long-arg, --initialize, --render 6 | 7 | long options can also be noted by a single dash: 8 | -long-arg, -initialize, -render 9 | 10 | if that's the case, short options cannot be chained: -sdr 11 | short arguments will always need to be separated by a space 12 | 13 | another way to look at it, a dash denotes a switch 14 | -long-arg, -initialize, -render, -s, -d, -r 15 | 16 | arguments without a preceeding hyphen '-' are option dependent arguments: 17 | -render list all ("list" and "all" being arguments to the option "render") 18 | 19 | similar for double-hypen options 20 | --render list all ("list" and "all" being arguments to the option "render") 21 | 22 | with long arguments using double-hypen "--": 23 | chained short options can have arguments, but only for the last in a chain 24 | 25 | to signal the end of option parsing (and use it as an argument for the 26 | command itself) use double-hyphen "--" 27 | 28 | --]] 29 | 30 | local type, error = type, error 31 | 32 | local function parse_args(args) 33 | if type(args) ~= "table" then 34 | error("invalid argument, must pass argument table") 35 | end 36 | error("not implimented") 37 | end 38 | 39 | return parse_args 40 | -------------------------------------------------------------------------------- /data/utils/color.lua: -------------------------------------------------------------------------------- 1 | --Copyright 2021 Scott Smith 2 | -- 3 | --Simply converts hex strings to floating point rgba values and back 4 | 5 | local tonumber = tonumber 6 | local floor = math.floor 7 | local sfmt = string.format 8 | local sfind = string.find 9 | 10 | local find_str = "^#?(%x%x)(%x%x)(%x%x)(%x?%x?)$" 11 | 12 | local FF_inv = 1.0/0xff 13 | 14 | 15 | local function hex2rgba(str) 16 | local s, e, r, g, b, a = sfind(str or "", find_str) 17 | if not s then return end 18 | 19 | return 20 | tonumber(r, 16) * FF_inv, 21 | tonumber(g, 16) * FF_inv, 22 | tonumber(b, 16) * FF_inv, 23 | #a>0 and tonumber(a, 16) * FF_inv or nil 24 | end 25 | 26 | local function rgba2hex(r,g,b,a) 27 | return sfmt("#%02X%02X%02X", 28 | floor(r * 0xff + 0.5), 29 | floor(g * 0xff + 0.5), 30 | floor(b * 0xff + 0.5)) .. 31 | (a and sfmt("%02X", floor(a * 0xff + 0.5)) or "") 32 | end 33 | 34 | return { 35 | hex2rgba = hex2rgba, 36 | rgba2hex = rgba2hex 37 | } 38 | -------------------------------------------------------------------------------- /data/utils/io.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021 Scott Smith 2 | -- Lua uses Window's ANSI api to open files. I want to change that. 3 | 4 | 5 | local ffi = require("ffi") 6 | if ffi.os ~= "Windows" then return io end 7 | 8 | local setmetatable = setmetatable 9 | local type = type 10 | local sfmt = string.format 11 | 12 | local m = {} 13 | 14 | if not hal_defined.io_function then 15 | hal_defined.io_function = true 16 | ffi.cdef[[ 17 | typedef unsigned int UINT; 18 | typedef unsigned long DWORD; 19 | typedef DWORD *LPDWORD; 20 | 21 | typedef long LONG; 22 | typedef int64_t LONGLONG; 23 | typedef uint64_t ULONGLONG; 24 | 25 | typedef void *LPVOID; 26 | typedef const void *LPCVOID; 27 | typedef void *HANDLE; 28 | typedef HANDLE *PHANDLE; 29 | 30 | typedef char *LPSTR; 31 | typedef const char *LPCCH; 32 | 33 | typedef wchar_t WCHAR; 34 | typedef WCHAR *LPWSTR; 35 | typedef const WCHAR *LPCWSTR; 36 | typedef const WCHAR *LPCWCH; 37 | typedef int BOOL; 38 | typedef BOOL *LPBOOL; 39 | typedef union _LARGE_INTEGER { 40 | struct { 41 | DWORD LowPart; 42 | LONG HighPart; 43 | } DUMMYSTRUCTNAME; 44 | struct { 45 | DWORD LowPart; 46 | LONG HighPart; 47 | } u; 48 | LONGLONG QuadPart; 49 | } LARGE_INTEGER; 50 | typedef LARGE_INTEGER *PLARGE_INTEGER; 51 | 52 | typedef struct _SECURITY_ATTRIBUTES { 53 | DWORD nLength; 54 | LPVOID lpSecurityDescriptor; 55 | BOOL bInheritHandle; 56 | } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; 57 | 58 | typedef void *LPOVERLAPPED; 59 | 60 | int MultiByteToWideChar( 61 | UINT CodePage, 62 | DWORD dwFlags, 63 | LPCCH lpMultiByteStr, 64 | int cbMultiByte, 65 | LPWSTR lpWideCharStr, 66 | int cchWideChar 67 | ); 68 | int WideCharToMultiByte( 69 | UINT CodePage, 70 | DWORD dwFlags, 71 | LPCWCH lpWideCharStr, 72 | int cchWideChar, 73 | LPSTR lpMultiByteStr, 74 | int cchMultiByte, 75 | LPCCH default, 76 | LPBOOL used 77 | ); 78 | 79 | 80 | HANDLE CreateFileW( 81 | LPCWSTR lpFileName, 82 | DWORD dwDesiredAccess, 83 | DWORD dwShareMode, 84 | LPSECURITY_ATTRIBUTES lpSecurityAttributes, 85 | DWORD dwCreationDisposition, 86 | DWORD dwFlagsAndAttributes, 87 | HANDLE hTemplateFile 88 | ); 89 | 90 | BOOL GetFileSizeEx( 91 | HANDLE hFile, 92 | PLARGE_INTEGER lpFileSize 93 | ); 94 | BOOL ReadFile( 95 | HANDLE hFile, 96 | LPVOID lpBuffer, 97 | DWORD nNumberOfBytesToRead, 98 | LPDWORD lpNumberOfBytesRead, 99 | LPOVERLAPPED lpOverlapped 100 | ); 101 | BOOL WriteFile( 102 | HANDLE hFile, 103 | LPCVOID lpBuffer, 104 | DWORD nNumberOfBytesToWrite, 105 | LPDWORD lpNumberOfBytesWritten, 106 | LPOVERLAPPED lpOverlapped 107 | ); 108 | BOOL CloseHandle(HANDLE hObject); 109 | 110 | DWORD GetLastError(); 111 | 112 | DWORD FormatMessageW( 113 | DWORD dwFlags, 114 | LPCVOID lpSource, 115 | DWORD dwMessageId, 116 | DWORD dwLanguageId, 117 | LPWSTR lpBuffer, 118 | DWORD nSize, 119 | va_list *Arguments 120 | ); 121 | 122 | void *malloc(size_t size); 123 | void free(void *memblock); 124 | 125 | ]] 126 | end 127 | local CP_UTF8 = 65001 128 | local WIDE_CHAR_SIZE = 2 129 | 130 | local GENERIC_READ = 0x80000000 --(0x80000000L) 131 | local GENERIC_WRITE = 0x40000000 --(0x40000000L) 132 | 133 | -- to prevent other processes from opening use value of 0 134 | local FILE_SHARE_READ = 0x00000001 135 | local FILE_SHARE_WRITE = 0x00000002 136 | local FILE_SHARE_DELETE = 0x00000004 137 | 138 | local CREATE_NEW = 1 139 | local CREATE_ALWAYS = 2 140 | local OPEN_EXISTING = 3 141 | local OPEN_ALWAYS = 4 142 | local TRUNCATE_EXISTING = 5 143 | 144 | local FILE_ATTRIBUTE_NORMAL = 0x00000080 145 | 146 | local INVALID_HANDLE_VALUE = ffi.cast("HANDLE", -1) 147 | 148 | 149 | local FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 150 | local FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 151 | 152 | local C = ffi.C 153 | 154 | local function utf8_to_wide(str) 155 | local characters = C.MultiByteToWideChar(CP_UTF8, 0, str, #str, nil, 0) 156 | --add WIDE_CHAR_SIZE to characters for zero 157 | local buf = ffi.new("WCHAR[?]",characters + WIDE_CHAR_SIZE) 158 | C.MultiByteToWideChar(CP_UTF8, 0, str, #str, buf, characters) 159 | return buf 160 | end 161 | 162 | local function wide_to_utf8(wstr) 163 | local size = C.WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nil,0,nil,nil) 164 | local buf = ffi.new("char[?]", size + 1) --add one for zero 165 | C.WideCharToMultiByte(CP_UTF8, 0, wstr, -1, buf, size, nil, nil) 166 | return ffi.string(buf, size) 167 | end 168 | 169 | local bit = require "bit" 170 | local bor = bit.bor 171 | 172 | local function get_last_error_msg() 173 | --TODO:use FormatMessage() for last error message 174 | local err = C.GetLastError() 175 | local bufsize = 1024 176 | local buf = ffi.new("WCHAR[?]", bufsize + WIDE_CHAR_SIZE) 177 | C.FormatMessageW( 178 | bor(FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS), 179 | nil, 180 | err, 181 | 0, 182 | buf, bufsize, 183 | nil 184 | ) 185 | return wide_to_utf8(buf) 186 | --return sfmt("error code (%d)", C.GetLastError()) 187 | end 188 | 189 | function m:size() 190 | if not self.handle then return nil, "no file open" end 191 | if not self.length then 192 | self.length = ffi.new("LARGE_INTEGER[1]") 193 | end 194 | local ret = C.GetFileSizeEx(self.handle, self.length) ~= 0 195 | if not ret then 196 | return nil, get_last_error_msg() 197 | end 198 | return self.length[0].QuadPart 199 | end 200 | 201 | --TODO: support other Lua read arguments: *l, *n 202 | --TODO: check what happens if reading from EOF 203 | --TODO: this will likely all break if it can't all be loaded into memory 204 | function m:read(num) 205 | if not self.handle then return nil, "no file open" end 206 | --print("reading HANDLE", self.handle) 207 | local size = 0 208 | if type(num) == "number" then 209 | size = num 210 | elseif type(num) == "string" then 211 | if num == "*a" then 212 | local err 213 | size, err = self:size() 214 | if not size then return nil, err end 215 | elseif num == "*l" then 216 | error("*l not supported") 217 | elseif num == "*n" then 218 | error("*n not supported") 219 | end 220 | elseif type(num) == "nil" then 221 | error("*l (default) not supported") 222 | end 223 | local size = self:size() 224 | local buf = ffi.gc(C.malloc(size), C.free) 225 | local bytesread = ffi.new("DWORD[1]", 0) 226 | local ret = C.ReadFile(self.handle, buf, size, bytesread, nil) ~= 0 227 | if not ret then return nil, get_last_error_msg() end 228 | return ffi.string(buf, bytesread[0]), bytesread[0] 229 | end 230 | 231 | function m:write(str) 232 | if not self.handle then return nil, "no file open" end 233 | local byteswritten = ffi.new("DWORD[1]", 0) 234 | local ret = C.WriteFile(self.handle, str, #str, byteswritten, nil) ~= 0 235 | if not ret then return nil, get_last_error_msg() end 236 | return byteswritten[0] 237 | end 238 | 239 | function m:close() 240 | if self.handle then 241 | local ret = C.CloseHandle(self.handle) ~= 0 242 | self.handle = false 243 | if not ret then 244 | return nil, get_last_error_msg() 245 | end 246 | end 247 | return true 248 | end 249 | 250 | local f = {} 251 | 252 | --modes: 253 | -- r Read (beginning of file) 254 | -- r+ Read & Write (beginning of file) 255 | -- w Write Truncate to zero length or created. (beginning of file) 256 | -- w+ Read & Write. Truncate to zero or created (beginning of file) 257 | -- a Append. Create if not exist. (end of file) 258 | -- a+ Read (beginning of file) & Append (end of file). Create if not exist. 259 | -- Append only allows writing at end of file 260 | function f.open(filename, mode) 261 | --print("OPEN", filename, mode) 262 | local ft = {} 263 | local read, write, create, append, trunc = false, false, false, false, false 264 | 265 | local desired_access --set by parsing mode 266 | local share_mode = bor(FILE_SHARE_READ, FILE_SHARE_WRITE) 267 | local creation_disposition --set by parsing mode 268 | local flags_and_attributes = FILE_ATTRIBUTE_NORMAL 269 | 270 | do --{{{ parse mode 271 | mode = mode or "r" 272 | local s, e, rwa, bp1, bp2 = string.find(mode, "([rwa])([%+b]?)([%+b]?)") 273 | if rwa == "r" then 274 | desired_access = GENERIC_READ 275 | creation_disposition = OPEN_EXISTING 276 | read = true 277 | elseif rwa == "w" then 278 | desired_access = GENERIC_WRITE 279 | creation_disposition = CREATE_ALWAYS 280 | write = true 281 | trunc = true 282 | create = true 283 | elseif rwa == "a" then 284 | desired_access = GENERIC_WRITE 285 | creation_disposition = CREATE_ALWAYS 286 | write = true 287 | append = true 288 | create = true 289 | end 290 | if bp1 == "+" or bp2 == "+" then 291 | desired_access = bor(GENERIC_READ, GENERIC_WRITE) 292 | read, write = true, true 293 | end 294 | end--}}} parse mode 295 | if not desired_access or not creation_disposition then 296 | return nil, "invalid mode" 297 | end 298 | 299 | local handle = C.CreateFileW( 300 | utf8_to_wide(filename), 301 | desired_access, share_mode, 302 | nil, --security attributes 303 | creation_disposition, flags_and_attributes, 304 | nil -- template file 305 | ) 306 | 307 | --print("HANDLE opened", handle) 308 | --print("INVALID_HANDLE", INVALID_HANDLE_VALUE) 309 | if handle == INVALID_HANDLE_VALUE then 310 | return nil, get_last_error_msg() 311 | end 312 | 313 | ft.handle = handle 314 | 315 | return setmetatable(ft, {__index = m}) 316 | 317 | end 318 | 319 | return setmetatable({}, {__index = f}) 320 | -------------------------------------------------------------------------------- /data/utils/logging.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2020 -- Scott Smith -- 2 | 3 | local f = {} 4 | f._VERSION = "Logging 0.3.0-beta.2" 5 | 6 | local setmetatable, type, require 7 | = setmetatable, type, require 8 | local io_write, dgetinfo = io.write, debug.getinfo 9 | local strup = string.upper 10 | local sfmt = string.format 11 | local osdate = os.date 12 | local floor = math.floor 13 | 14 | local printf = require "utils.printf" 15 | 16 | function f.time (...) 17 | io_write(osdate("[%m/%d %H:%M:%S] ")) 18 | printf(...) 19 | io_write("\n") 20 | end 21 | 22 | local log_print = function (self, level, logtype, ...) 23 | local di = dgetinfo(level + 2, "Sl"); 24 | local name = di.short_src 25 | local line_number = di.currentline 26 | if self._hal then --{{{ agpack specific modifications 27 | --TODO: separate agpack specific mods to make logging a generic util 28 | --something like: string = function(name, line_number, logtype) 29 | local agpack = name:match("agpack(.*).lua") 30 | if agpack then 31 | name = sfmt("AGP%s", agpack) 32 | end 33 | local hal = self._hal 34 | name = sfmt("[%2d'%02d]", floor(hal.met), hal.frame) .. name 35 | end --}}} 36 | printf("%s:%-4d %5s: ", name, line_number, logtype) 37 | printf(...) 38 | io_write("\n") 39 | end 40 | 41 | local log = setmetatable(f, {__index = function(self, logtype) 42 | logtype = strup(logtype) 43 | return function(level, ...) 44 | if type(level) == "number" then 45 | return log_print(self, level, logtype, ...) 46 | else 47 | return log_print(self, 0, logtype, level, ...) 48 | end 49 | end 50 | end; 51 | __call = function(self, options) 52 | self._hal = options.hal 53 | return self 54 | end}) 55 | 56 | return log 57 | -------------------------------------------------------------------------------- /data/utils/math/round.lua: -------------------------------------------------------------------------------- 1 | local ceil, floor = math.ceil, math.floor 2 | 3 | local function round(num) 4 | if num < 0 then 5 | return ceil(num - 0.5) 6 | end 7 | return floor(num + 0.5) 8 | end 9 | 10 | return round 11 | -------------------------------------------------------------------------------- /data/utils/math/stats.lua: -------------------------------------------------------------------------------- 1 | local sqrt = math.sqrt 2 | 3 | local function average(list) 4 | local count = list.count or #list or 0 5 | local sum = 0 6 | for i = 1, count do 7 | sum = sum + list[i] 8 | end 9 | return sum / count 10 | end 11 | 12 | local function stddev(list, avg) 13 | avg = avg or average(list) 14 | local sum = 0 15 | local count = list.count or #list or 0 16 | for i = 1, count do 17 | local diff = list[i] - avg 18 | sum = sum + diff * diff 19 | end 20 | return sqrt(sum / count) 21 | end 22 | 23 | return { 24 | average = average, 25 | stddev = stddev, 26 | } 27 | -------------------------------------------------------------------------------- /data/utils/math/truncate.lua: -------------------------------------------------------------------------------- 1 | local ceil, floor = math.ceil, math.floor 2 | 3 | local function truncate(num) 4 | if num < 0 then 5 | return ceil(num) 6 | end 7 | return floor(num) 8 | end 9 | 10 | return truncate 11 | -------------------------------------------------------------------------------- /data/utils/printf.lua: -------------------------------------------------------------------------------- 1 | local iowrite, sfmt = io.write, string.format 2 | 3 | local function printf(...) 4 | return iowrite(sfmt(...)) 5 | end 6 | 7 | return printf 8 | -------------------------------------------------------------------------------- /data/utils/string/check_utf8.lua: -------------------------------------------------------------------------------- 1 | local hex_dump = require "utils.string.hex_dump" 2 | local utf8 = require "utf8" 3 | local sfmt = string.format 4 | local print = print 5 | local error = error 6 | 7 | --TODO: don't think this should throw an error, rather do return nil, message 8 | 9 | local function check_utf8(utf8str) 10 | local len, errpos = utf8.len(utf8str) 11 | if not len then 12 | print("ERROR:", utf8str) 13 | local hex = hex_dump(utf8str) 14 | error(sfmt("invalid utf8 at position %s\n%s", errpos, hex)) 15 | end 16 | end 17 | 18 | return check_utf8 19 | -------------------------------------------------------------------------------- /data/utils/string/hex_dump.lua: -------------------------------------------------------------------------------- 1 | local sfmt = string.format 2 | local ceil = math.ceil 3 | local min = math.min 4 | --local ti = table.insert 5 | local tcat = table.concat 6 | local select = select 7 | --local iowrite = io.write 8 | local iowrite = function() end 9 | 10 | local t = {} 11 | local t_count = 0 12 | 13 | local iow = function (...) 14 | for i = 1, select("#", ...) do 15 | local s = select(i, ...) 16 | t_count = t_count + 1 17 | t[t_count] = s 18 | --ti(t, s) 19 | end 20 | iowrite(...) 21 | end 22 | 23 | local function align(n) 24 | return ceil(n / 16) * 16 25 | end 26 | 27 | local function hex_dump(buf, first, last) 28 | first = first or 1 29 | last = last or #buf 30 | t_count = 0 31 | for i = align(first - 16) + 1, align(min(last, #buf)) do 32 | if (i-1) % 16 == 0 then iow(sfmt("%08X ", i-1)) end 33 | iow( i > #buf and " " or sfmt("%02X ", buf:byte(i)) ) 34 | if i % 8 == 0 then iow(" ") end 35 | if i % 16 == 0 then iow(buf:sub(i-16+1, i):gsub("%c","."),"\n") end 36 | end 37 | return tcat(t, "", 1, t_count) 38 | end 39 | 40 | return hex_dump 41 | -------------------------------------------------------------------------------- /data/utils/string/trim.lua: -------------------------------------------------------------------------------- 1 | 2 | --http://lua-users.org/wiki/StringTrim 3 | local match = string.match 4 | 5 | local function trim(s) 6 | if not s then return end 7 | return match(s,'^()%s*$') and '' or match(s,'^%s*(.*%S)') 8 | end 9 | 10 | return trim 11 | -------------------------------------------------------------------------------- /dist/LOOP License.txt: -------------------------------------------------------------------------------- 1 | LOOP License 2 | ----------- 3 | 4 | LOOP is licensed under the terms of the MIT license reproduced below. 5 | This means that LOOP is free software and can be used for both academic 6 | and commercial purposes at absolutely no cost. 7 | 8 | =============================================================================== 9 | 10 | Copyright (C) 2004-2008 Tecgraf, PUC-Rio. 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 16 | the Software, and to permit persons to whom the Software is furnished to do so, 17 | subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 24 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 25 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 26 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 27 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | =============================================================================== 30 | 31 | (end of LICENSE) 32 | -------------------------------------------------------------------------------- /dist/Vector.lua License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2013 Matthias Richter 2 | 3 | MIT License 4 | Full license text can be found in in /src/Vector.lua 5 | -------------------------------------------------------------------------------- /dist/bump.lua License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Enrique García Cota 2 | 3 | Full license text can be found in /src/libs/bump.lua 4 | -------------------------------------------------------------------------------- /dist/json License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 rxi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /game.cfg: -------------------------------------------------------------------------------- 1 | ; [ AGPACK Cartridge System ] ; 2 | FPS=0 3 | UNDO=0 4 | 5 | ;Double Click Time, in seconds (default: 0.5) 6 | DOUBLE_CLICK_SPEED=0.5 7 | 8 | ;Wrap width in pixels. Comment out or leave blank for no auto wrapping. 9 | SERIF_AUTOWRAP=400px 10 | FIXED_AUTOWRAP=500px 11 | WRAP_LIMIT_MINIMUM=100px 12 | 13 | ;serif or fixed 14 | TYPEFACE=serif 15 | 16 | ; BULLET STYLE: circle, square, or both 17 | BULLET_STYLE=both 18 | 19 | ; GRID: line, dot, or false 20 | GRID=dot 21 | ; GRID_MAJOR: screen, cell_count_width, cell_count_height, or false 22 | ; ex: GRID_MAJOR=screen ; grid is same width and height as the window 23 | ; GRID_MAJOR=half ; grid is half the width and height as the window 24 | ; GRID_MAJOR=30, 20 ; draw grid line every 30 cells wide, 20 cells high 25 | GRID_MAJOR=64,64 26 | ; GRID_MAJOR_WEIGHT: thin or thick 27 | GRID_MAJOR_WEIGHT=thick 28 | 29 | ;; DEFAULT THEME ;; 30 | ;; This is the default theme. 31 | ;; The color string is a hex string with optional alpha channel 32 | ;; #RRGGBBAA R = Red, G = Green, B = Blue, A = Alpha (optional) 33 | 34 | ;COLOR_FG=#DECEC6 35 | ;COLOR_BG=#4b3a47 36 | ;COLOR_GRID_DOT=#decec660 37 | ;COLOR_GRID_LINE=#decec618 38 | ;COLOR_GRID_LINE_ORIGIN=#decec642 39 | ;COLOR_BOX=#2B1B27 40 | ;COLOR_RECTANGLE_SELECTION=#DECEC6 41 | ;COLOR_TEXT=#DECEC6 42 | ;COLOR_TEXT_SELECT=#decec62e 43 | ;COLOR_TASK=#DECEC6 44 | ;COLOR_TASK_CANCEL=#e09aa4 45 | ;COLOR_EDIT_OUTLINE=#CB5264 46 | ;COLOR_EDIT_CURSOR=#DECEC6 47 | ;COLOR_WAYPOINT=#352230 48 | ;COLOR_FOCUS_OUTLINE=#decec6 49 | ; 50 | ;COLOR_STATUS_BG=#2b1b27 51 | ;COLOR_STATUS_FG=#DECEC6 52 | ; 53 | ;COLOR_DIALOG_BG=#2B1B27 54 | ;COLOR_DIALOG_BUTTON=#DECEC6 55 | ;COLOR_DIALOG_BUTTON_TEXT=#2B1B27 56 | ;COLOR_DIALOG_HIGHLIGHT=#CB5264 57 | 58 | ;; LIGHT THEME ;; 59 | ; To use this theme, uncomment the options below 60 | COLOR_FG=#1c2830 61 | COLOR_BG=#d2bcca 62 | COLOR_GRID_DOT =#1c2830ff 63 | COLOR_GRID_LINE=#1c283018 64 | COLOR_GRID_LINE_ORIGIN=#1c283042 65 | COLOR_BOX=#eae3ed 66 | COLOR_RECTANGLE_SELECTION=#314654 67 | COLOR_TEXT=#1c2830 68 | COLOR_TEXT_SELECT=#1c28302e 69 | COLOR_TASK=#1c2830 70 | COLOR_TASK_CANCEL=#d1102d 71 | COLOR_EDIT_OUTLINE=#d1102d 72 | COLOR_EDIT_CURSOR=#1c2830 73 | COLOR_WAYPOINT=#dcd0e2 74 | COLOR_FOCUS_OUTLINE=#051824a0 75 | COLOR_FOCUS_OUTLINE_HOVER=#45235890 76 | 77 | COLOR_STATUS_BG=#2b1b27 78 | COLOR_STATUS_FG=#DECEC6 79 | 80 | COLOR_DIALOG_BG=#2B1B27 81 | COLOR_DIALOG_BUTTON=#eae3ed 82 | COLOR_DIALOG_BUTTON_TEXT=#2B1B27 83 | COLOR_DIALOG_HIGHLIGHT=#CB5264 84 | 85 | 86 | 87 | DEBUG_QUEUE_DETAIL=true 88 | 89 | ;; Modify the option below to break the program 90 | PACK=link 91 | -------------------------------------------------------------------------------- /link.agpack/assets/data/default.link: -------------------------------------------------------------------------------- 1 | ; Generated by LINK! Alpha 0.6.2 2 | [meta] 3 | version = prototype 4 | [camera] 5 | w = 854 6 | h = 480 7 | dx = 0 8 | dy = 0 9 | scale = 0 10 | [font] 11 | name = SourceCodePro-Regular.ttf 12 | size = 16 13 | [note.1] 14 | x = 14 15 | y = 7 16 | w = 14 17 | h = 1 18 | date = 2021-08-27 19 | time = 12:41:26 PM 20 | note = Welcome to LINK! L.I.N.K. 21 | type = note 22 | [note.2] 23 | x = 13 24 | y = 12 25 | w = 16 26 | h = 1 27 | date = 2021-08-27 28 | time = 12:43:38 PM 29 | note = Would you like to learn more? 30 | type = note 31 | [note.3] 32 | x = 6 33 | y = 16 34 | w = 32 35 | h = 2 36 | date = 2021-08-27 37 | time = 12:44:05 PM 38 | note = Drag the file 'Welcome.link', found in the same directory this\n fine product is located, and drop it here, on this program. 39 | type = note 40 | [note.4] 41 | x = 18 42 | y = 7 43 | w = 9 44 | h = 1 45 | date = 2021-08-27 46 | time = 01:21:34 PM 47 | note = . -_v ° .I.N.K. 48 | type = note 49 | [note.5] 50 | x = 28 51 | y = 23 52 | w = 1 53 | h = 2 54 | date = 2021-08-27 55 | time = 01:23:40 PM 56 | note = \n 57 | type = note 58 | [note.6] 59 | x = 28 60 | y = 24 61 | w = 4 62 | h = 1 63 | date = 2021-08-27 64 | time = 01:23:55 PM 65 | note =   66 | type = note 67 | [note.7] 68 | x = 31 69 | y = 25 70 | w = 1 71 | h = 7 72 | date = 2021-08-27 73 | time = 01:24:01 PM 74 | note = \n\n\n\n\n\n 75 | type = note 76 | [note.8] 77 | x = 21 78 | y = 31 79 | w = 22 80 | h = 1 81 | date = 2021-08-27 82 | time = 01:24:08 PM 83 | note =   84 | type = note 85 | [note.9] 86 | x = 41 87 | y = 31 88 | w = 1 89 | h = 13 90 | date = 2021-08-27 91 | time = 01:24:16 PM 92 | note = \n\n\n\n\n\n\n\n\n\n\n\n 93 | type = note 94 | [note.10] 95 | x = 42 96 | y = 31 97 | w = 1 98 | h = 4 99 | date = 2021-08-27 100 | time = 01:24:25 PM 101 | note = \n\n\n 102 | type = note 103 | [note.11] 104 | x = 35 105 | y = 31 106 | w = 1 107 | h = 7 108 | date = 2021-08-27 109 | time = 01:24:41 PM 110 | note = \n\n\n\n\n\n 111 | type = note 112 | [note.12] 113 | x = 21 114 | y = 31 115 | w = 1 116 | h = 11 117 | date = 2021-08-27 118 | time = 01:24:49 PM 119 | note = \n\n\n\n\n\n\n\n\n\n 120 | type = note 121 | [note.13] 122 | x = 41 123 | y = 42 124 | w = 16 125 | h = 1 126 | date = 2021-08-27 127 | time = 01:25:07 PM 128 | note =   129 | type = note 130 | [note.14] 131 | x = 49 132 | y = 44 133 | w = 4 134 | h = 1 135 | date = 2021-08-27 136 | time = 01:25:26 PM 137 | note = i feel 138 | type = note 139 | [note.15] 140 | x = 12 141 | y = -14 142 | w = 1 143 | h = 15 144 | date = 2021-08-27 145 | time = 01:47:38 PM 146 | note = \n\n\n\n\n\n\n\n\n\n\n\n\n\n 147 | type = note 148 | [note.16] 149 | x = -10 150 | y = 6 151 | w = 11 152 | h = 1 153 | date = 2021-08-27 154 | time = 01:47:42 PM 155 | note =   156 | type = note 157 | [note.17] 158 | x = -2 159 | y = 16 160 | w = 3 161 | h = 1 162 | date = 2021-08-27 163 | time = 01:47:43 PM 164 | note =   165 | type = note 166 | [note.18] 167 | x = 42 168 | y = 16 169 | w = 9 170 | h = 1 171 | date = 2021-08-27 172 | time = 01:47:47 PM 173 | note =   174 | type = note 175 | [note.19] 176 | x = 42 177 | y = 6 178 | w = 7 179 | h = 1 180 | date = 2021-08-27 181 | time = 01:47:42 PM 182 | note =   183 | type = note 184 | [note.20] 185 | x = 45 186 | y = 6 187 | w = 1 188 | h = 11 189 | date = 2021-08-27 190 | time = 01:54:23 PM 191 | note = \n\n\n\n\n\n\n\n\n\n 192 | type = note 193 | [note.21] 194 | x = 48 195 | y = 16 196 | w = 1 197 | h = 9 198 | date = 2021-08-27 199 | time = 01:54:43 PM 200 | note = \n\n\n\n\n\n\n\n 201 | type = note 202 | [note.22] 203 | x = 30 204 | y = -8 205 | w = 1 206 | h = 9 207 | date = 2021-08-27 208 | time = 01:54:57 PM 209 | note = \n\n\n\n\n\n\n\n 210 | type = note 211 | [note.23] 212 | x = 45 213 | y = 10 214 | w = 11 215 | h = 1 216 | date = 2021-08-27 217 | time = 01:55:15 PM 218 | note =   219 | type = note 220 | [note.24] 221 | x = 52 222 | y = 2 223 | w = 1 224 | h = 9 225 | date = 2021-08-27 226 | time = 01:55:26 PM 227 | note = \n\n\n\n\n\n\n\n 228 | type = note 229 | [note.25] 230 | x = 52 231 | y = 4 232 | w = 9 233 | h = 1 234 | date = 2021-08-27 235 | time = 01:55:30 PM 236 | note =   237 | type = note 238 | [note.26] 239 | x = 18 240 | y = 12 241 | w = 5 242 | h = 1 243 | date = 2021-08-27 244 | time = 01:56:00 PM 245 | note = li'e -. 246 | type = note 247 | [note.27] 248 | x = 30 249 | y = 24 250 | w = 2 251 | h = 2 252 | date = 2021-08-27 253 | time = 03:35:43 PM 254 | note =  \n 255 | type = note 256 | [note.28] 257 | x = 11 258 | y = 23 259 | w = 1 260 | h = 6 261 | date = 2021-08-28 262 | time = 01:48:33 PM 263 | note = \n\n\n\n\n 264 | type = note 265 | [note.29] 266 | x = -2 267 | y = 16 268 | w = 1 269 | h = 13 270 | date = 2021-08-28 271 | time = 01:49:05 PM 272 | note = \n\n\n\n\n\n\n\n\n\n\n\n 273 | type = note 274 | [note.30] 275 | x = -13 276 | y = 28 277 | w = 14 278 | h = 1 279 | date = 2021-08-28 280 | time = 01:49:12 PM 281 | note =   282 | type = note 283 | [note.31] 284 | x = -6 285 | y = -2 286 | w = 1 287 | h = 9 288 | date = 2021-08-28 289 | time = 01:49:32 PM 290 | note = \n\n\n\n\n\n\n\n 291 | type = note 292 | [note.32] 293 | x = -8 294 | y = 6 295 | w = 1 296 | h = 11 297 | date = 2021-08-28 298 | time = 01:49:38 PM 299 | note = \n\n\n\n\n\n\n\n\n\n 300 | type = note 301 | [note.33] 302 | x = -18 303 | y = 11 304 | w = 11 305 | h = 1 306 | date = 2021-08-28 307 | time = 01:49:42 PM 308 | note =   309 | type = note 310 | [note.34] 311 | x = 2 312 | y = -9 313 | w = 11 314 | h = 1 315 | date = 2021-08-28 316 | time = 01:49:58 PM 317 | note =   318 | type = note 319 | [note.35] 320 | x = 6 321 | y = -22 322 | w = 1 323 | h = 14 324 | date = 2021-08-28 325 | time = 01:50:03 PM 326 | note = \n\n\n\n\n\n\n\n\n\n\n\n\n 327 | type = note 328 | [note.36] 329 | x = 30 330 | y = -7 331 | w = 13 332 | h = 1 333 | date = 2021-08-28 334 | time = 01:50:09 PM 335 | note =   336 | type = note 337 | [note.37] 338 | x = 39 339 | y = -25 340 | w = 1 341 | h = 19 342 | date = 2021-08-28 343 | time = 01:50:21 PM 344 | note = \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n 345 | type = note 346 | [note.38] 347 | x = 39 348 | y = -22 349 | w = 13 350 | h = 1 351 | date = 2021-08-28 352 | time = 01:50:26 PM 353 | note =   354 | type = note 355 | [note.39] 356 | x = 45 357 | y = -32 358 | w = 1 359 | h = 11 360 | date = 2021-08-28 361 | time = 01:50:32 PM 362 | note = \n\n\n\n\n\n\n\n\n\n 363 | type = note 364 | [note.40] 365 | x = 34 366 | y = -15 367 | w = 6 368 | h = 1 369 | date = 2021-08-28 370 | time = 01:50:35 PM 371 | note =   372 | type = note 373 | [note.41] 374 | x = 6 375 | y = -19 376 | w = 8 377 | h = 1 378 | date = 2021-08-28 379 | time = 01:50:42 PM 380 | note =   381 | type = note 382 | [note.42] 383 | x = 0 384 | y = 23 385 | w = 43 386 | h = 1 387 | date = 2021-08-28 388 | time = 01:51:04 PM 389 | note =   390 | type = note 391 | [note.43] 392 | x = 0 393 | y = 0 394 | w = 1 395 | h = 24 396 | date = 2021-08-28 397 | time = 01:51:13 PM 398 | note = \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n 399 | type = note 400 | [note.44] 401 | x = 42 402 | y = 1 403 | w = 1 404 | h = 24 405 | date = 2021-08-28 406 | time = 01:51:19 PM 407 | note = \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n 408 | type = note 409 | [note.45] 410 | x = 0 411 | y = 1 412 | w = 43 413 | h = 1 414 | date = 2021-08-28 415 | time = 01:51:36 PM 416 | note =   417 | type = note 418 | [note.46] 419 | x = 40 420 | y = 22 421 | w = 1 422 | h = 4 423 | date = 2021-09-03 424 | time = 07:01:53 PM 425 | note = \n\n\n 426 | type = note 427 | -------------------------------------------------------------------------------- /link.agpack/assets/data/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akciom/link/ad7e126233d71c8f1542c32fceffaaa28066ba95/link.agpack/assets/data/icon.png -------------------------------------------------------------------------------- /link.agpack/keybind.cfg: -------------------------------------------------------------------------------- 1 | [global] 2 | left = left 3 | right = right 4 | up = up 5 | down = down 6 | 7 | n = toggle_edit_mode 8 | backspace = backspace 9 | delete = delete 10 | escape = esc 11 | left_square_bracket = left_bracket 12 | right_square_bracket = right_bracket 13 | 14 | tab = tab 15 | return = enter 16 | end = end 17 | home = home 18 | rshift = shift 19 | lshift = shift 20 | lctrl = ctrl 21 | rctrl = ctrl 22 | s = save 23 | z = undo 24 | c = copy 25 | x = cut 26 | v = paste 27 | a = select_all 28 | ;Game Pad 29 | gp1_dpleft = left 30 | gp1_dpright = right 31 | gp1_dpup = up 32 | gp1_dpdown = down 33 | 34 | f12 = debug_menu 35 | -------------------------------------------------------------------------------- /link.agpack/src/Animation.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2020 -- Scott Smith -- 2 | -- 3 | -- Animation Library. Takes a sprite sheet generated by Aseprite and turns it 4 | -- into something digestable by LÖVE. 5 | --TODO: accept sprite atlases 6 | --TODO: generic invalid image frame that all sprites reference 7 | -- * space saving, so each sprite doesn't need an extra frame in atlas 8 | 9 | local setmetatable = setmetatable 10 | local random = math.random 11 | local sfmt = string.format 12 | 13 | local lg = love.graphics 14 | local json = require "json" 15 | 16 | local AG = hal_conf.AG 17 | 18 | local m = {} 19 | 20 | local DEFAULT_LOOP_IDX = 1 21 | local TAG_SIZE = 4 22 | 23 | local function update_quad(quad, state, dt) --{{{ 24 | if not state:find("^[%w_]+$") then error("invalid state name") end 25 | 26 | local timer = quad.timer + dt 27 | 28 | if timer >= quad.data.duration or state ~= quad.state then 29 | local bframe, eframe 30 | local direction = "forward" 31 | 32 | local idx = quad.idx + 1 33 | --print(state, "UPDATE QUAD1:", quad.state,"<>", quad.loop_state, idx) 34 | 35 | local tags = quad.data.tags 36 | local tagidx 37 | 38 | do 39 | local old_state = quad.state 40 | local new_state = state 41 | 42 | if old_state ~= new_state then 43 | local next_loop_state = "NEVER_SET"--new loopstate 44 | quad.ready = true 45 | if not tagidx then 46 | tagidx = tags.name[sfmt("%s:end.%s", old_state, new_state)] 47 | next_loop_state = old_state..":end." 48 | --TODO:Use ratio of completed start animation to set index when entering end tag 49 | -- 50 | --notes: if start animation has only ran for three out of four frames, 51 | --the end animation should start on the 2nd frame in. 52 | -- 53 | --local oldls = quad.loop_state 54 | --if oldls == ":start" then 55 | -- start_frame = floor((ratio of start frames played) * (total num end frames)) 56 | -- bframe = start frame for end frame 57 | -- idx = bframe + start_frame 58 | --end 59 | end 60 | if not tagidx then 61 | tagidx = tags.name[sfmt("%s:start", new_state)] 62 | next_loop_state = ":start" 63 | end 64 | if not tagidx then 65 | tagidx = tags.name[sfmt("%s:loop", new_state)] 66 | next_loop_state = ":loop" 67 | end 68 | if not tagidx then 69 | tagidx = tags.name[sfmt("%s:once", new_state)] 70 | next_loop_state = ":once" 71 | quad.ready = false 72 | end 73 | if not tagidx then 74 | tagidx = tags.name[new_state] 75 | next_loop_state = "" 76 | end 77 | if tagidx then 78 | idx = DEFAULT_LOOP_IDX 79 | else 80 | next_loop_state = "" 81 | end 82 | quad.loop_state = next_loop_state 83 | quad.state = state 84 | --states are now the same, next_loop_state is ready 85 | end 86 | end 87 | 88 | if not tagidx then 89 | local ls = quad.loop_state 90 | --print("NOT TAGIDX, QUAD/LOOP_STATE", quad.state, ls) 91 | local tagname = quad.state 92 | if ls == "" then 93 | tagidx = tags.name[tagname] 94 | end 95 | if ls == ":start" then 96 | tagname = quad.state .. ls 97 | tagidx = tags.name[tagname] 98 | eframe = tags[tagidx+2] 99 | if idx > eframe then 100 | ls = ":loop" 101 | end 102 | elseif ls:find(":end%.$") then 103 | tagname = ls .. quad.state 104 | tagidx = tags.name[tagname] 105 | --print("ENDTAGNAME", ls, quad.state, tagname, tagidx) 106 | eframe = tags[tagidx+2] 107 | --print("IDX, eframe", idx, eframe) 108 | if idx >= eframe then 109 | ls = "" 110 | --so next pass will find either a "", ":start", 111 | --or ":loop" loop_state 112 | quad.state = "none" 113 | end 114 | end 115 | if ls == ":loop" then 116 | tagname = quad.state .. ls 117 | tagidx = tags.name[tagname] 118 | end 119 | if ls == ":once" then 120 | tagname = quad.state .. ls 121 | tagidx = tags.name[tagname] 122 | eframe = tags[tagidx+2] 123 | if idx > eframe then 124 | idx = eframe 125 | quad.ready = true 126 | end 127 | end 128 | --print("TAG NAME", ls, tagname, tags.name[tagname]) 129 | quad.loop_state = ls 130 | end 131 | 132 | if tagidx then 133 | bframe = tags[tagidx+1] 134 | eframe = tags[tagidx+2] 135 | local ls = quad.loop_state 136 | if idx == DEFAULT_LOOP_IDX and ls == "" then 137 | --any animation that doesn't need a :start or :end should be 138 | --able to start any where in the animation and it'll look fine. 139 | --At least this is the assumption that I'm going to make. If 140 | --this is not the desired behavior, then add the :loop suffix. 141 | -- 142 | --This so enemy assets look more random. Say, when a few of them 143 | --enter an idle animation at the same time, animations will 144 | --start on a random frame. 145 | idx = bframe + random(0, eframe - bframe) 146 | end 147 | else 148 | bframe, eframe = DEFAULT_LOOP_IDX, DEFAULT_LOOP_IDX 149 | end 150 | --print("tag", state, tags[tagidx], bframe, eframe, direction) 151 | --direction: forward 152 | if idx < bframe or idx > eframe then idx = bframe end 153 | --TODO:direction: backward 154 | --TODO:direction: ping-pong 155 | quad.idx = idx 156 | timer = 0 157 | --print(state, "UPDATE QUAD2:", quad.state,"<>", quad.loop_state, idx) 158 | end 159 | --if quad.state == "forward" and quad.loop_state == ":loop" then error("FORWARD") end 160 | quad.timer = timer 161 | return quad 162 | end --}}} 163 | -- update animation to next frame 164 | function m:update(state, dt) 165 | --TODO:inline update_quad 166 | return update_quad(self.animation_state, state, dt) 167 | 168 | --instead of returning a quad, this function could update a spritebatch 169 | --although, this should update all animations at once and add them all 170 | --to the same sprite batch if I'm really going to do it properly 171 | end 172 | 173 | local sprite_data_memoize = {} 174 | 175 | function m:release() 176 | local anim = self.animation_state 177 | local fname = anim.data.fname 178 | local data = sprite_data_memoize[fname] 179 | sprite_data_memoize[fname] = nil 180 | data.sprite:release() 181 | data.sprite = nil 182 | self.tex = nil 183 | for i = 1, #data.quads do 184 | data.quads[i]:release() 185 | data.quads[i] = nil 186 | end 187 | data.quads = nil 188 | self.quad = nil 189 | end 190 | 191 | function m:mt__index(name) 192 | if name == "quad" then 193 | if self.tex then 194 | local anim = self.animation_state 195 | return anim.data[anim.idx] 196 | end 197 | return nil 198 | end 199 | return m[name] 200 | end 201 | 202 | local f = {} 203 | 204 | --TODO:organize quads, sprite_data, tags, etc. it's a mess 205 | local function load_sprite_sheet(fname) --{{{ 206 | if sprite_data_memoize[fname] then 207 | --print("Reusing existing sprite data: "..fname) 208 | local sdm = sprite_data_memoize[fname] 209 | return sdm.sprite, sdm.quads 210 | end 211 | local sprite = love.graphics.newImage(AG.assets.."/"..fname..".png") 212 | sprite:setFilter("linear", "nearest") 213 | 214 | local data do 215 | local f = io.open(AG.assets.."/"..fname..".json") 216 | local js 217 | if f then 218 | js = f:read("*a") 219 | f:close() 220 | else 221 | error("unable to open sprite meta data") 222 | end 223 | if js then 224 | data = json.decode(js) 225 | else 226 | error("unable to parse sprite meta data") 227 | end 228 | end 229 | 230 | if type(data) ~= "table" then 231 | error("invalid data for new_quad") 232 | end 233 | if #data.frames == 0 then 234 | error("invalid data format. export as array NOT hash") 235 | end 236 | 237 | local quad = {} 238 | quad.fname = fname 239 | local sprite_data = quad 240 | do --quad 241 | do --frame tags 242 | local tags = {count = 0, name = {}} 243 | quad.tags = tags 244 | 245 | local c = tags.count 246 | for i = 1, #data.meta.frameTags do 247 | local ft = data.meta.frameTags[i] 248 | local lname = ft.name:lower() 249 | tags.name[lname] = c 250 | tags[c+1] = ft.from + 1 251 | tags[c+2] = ft.to + 1 252 | tags[c+3] = lname 253 | tags[c+4] = ft.direction 254 | c = c + TAG_SIZE 255 | end 256 | tags.count = c 257 | end 258 | local duration = data.frames[1].duration 259 | local source_size = data.frames[1].sourceSize 260 | local w, h = sprite:getDimensions() 261 | for i = 1, #data.frames do 262 | local df = data.frames[i] 263 | --duration = df.duration 264 | 265 | local f = df.frame 266 | quad[i] = lg.newQuad(f.x,f.y, f.w,f.h, w, h) 267 | --print("newquad", i, quad[i]) 268 | end 269 | quad.duration = duration / 1000 270 | quad.source_size = source_size 271 | end 272 | 273 | sprite_data_memoize[fname] = { 274 | sprite = sprite, 275 | quads = sprite_data, 276 | } 277 | return sprite, sprite_data 278 | end --}}} 279 | 280 | function f:get_memoized_data() 281 | return sprite_data_memoize 282 | end 283 | 284 | function f:load(sprite_name, color, w_or_label, w_or_h, h) 285 | local t = {} 286 | --TODO:maybe the Animation class should only handle textures 287 | t.color = color 288 | if type(w_or_label) == "number" then 289 | --assume it's just a rectangle 290 | t.w = w_or_label 291 | t.h = w_or_h 292 | elseif type(w_or_label) == "string" then 293 | --assume it's a label, may have a rectangle 294 | t.label = w_or_label 295 | t.w = w_or_h 296 | t.h = h 297 | end 298 | 299 | if not sprite_name then 300 | --without a sprite, no metatable is needed 301 | return t 302 | end 303 | 304 | --TODO: Inline load_sprite_sheet 305 | local texture, quads = load_sprite_sheet(sprite_name) 306 | --set data.sprite 307 | t.tex = texture 308 | t.w = quads.source_size.w 309 | t.h = quads.source_size.h 310 | 311 | --TODO:animation data: probably should be in a component 312 | --set data.quad 313 | local anim = { 314 | data = quads, 315 | idx = 1, 316 | timer = 0, 317 | state = "idle", 318 | loop_state = "", 319 | ready = true, 320 | } 321 | 322 | local tagidx = quads.tags.name[anim.state] 323 | if tagidx then 324 | anim.idx = quads.tags[tagidx+1] 325 | end 326 | t.animation_state = anim 327 | 328 | return setmetatable(t, {__index = m.mt__index}) 329 | end 330 | 331 | return setmetatable(f, {__call = f.load}) 332 | -------------------------------------------------------------------------------- /link.agpack/src/Component.lua: -------------------------------------------------------------------------------- 1 | --Copyright 2020 -- Scott Smith -- 2 | 3 | local print, setmetatable, select 4 | = print, setmetatable, select 5 | local sfmt = string.format 6 | 7 | local f = {} 8 | 9 | local m = {type = "component"} 10 | function m:trunc(len) 11 | local eid = self.eid 12 | for i = len+1, #eid do 13 | eid[i] = nil 14 | end 15 | end 16 | 17 | --I could use a hash map, but the map will need to be updated to keep in 18 | --sync with the eid array so that's an optimization for later. 19 | 20 | --on the bright side, searching through arrays is fast and if it becomes 21 | --slow, the optimization should be quite easy! 22 | function m:map(entity) 23 | local eid = self.eid 24 | for i = 1, #eid do 25 | if eid[i] == entity then 26 | return i 27 | end 28 | end 29 | return false 30 | end 31 | 32 | function m:map_type(type, value) 33 | local t = self[type] 34 | if not t then return nil, "invalid type" end 35 | for i = 1, #t do 36 | if t[i] == value then 37 | return i 38 | end 39 | end 40 | return false 41 | end 42 | 43 | function m:print(list) local component = self 44 | list = list or {"eid"} 45 | local tstr = {sfmt("%5s:", "i")} 46 | for i = 1, #component[list[1]] do 47 | table.insert(tstr, sfmt("%3d", i)) 48 | end 49 | print(table.concat(tstr, ", ")) 50 | for i = 1, #tstr do tstr[i] = nil end 51 | for li = 1, #list do 52 | table.insert(tstr, sfmt("%5s:", list[li])) 53 | for i = 1, #component[list[li]] do 54 | local str 55 | local val = component[list[li]][i] 56 | if type(val) == "number" then 57 | str = sfmt("%3d", val) 58 | else 59 | str = tostring(val) 60 | end 61 | 62 | table.insert(tstr, str) 63 | end 64 | print(table.concat(tstr, ", ")) 65 | for i = 1, #tstr do tstr[i] = nil end 66 | end 67 | end --}}} 68 | 69 | function f:new(...) 70 | local t = {eid = {count = -1}} 71 | for i = 1, select("#", ...) do 72 | local li = select(i, ...) 73 | t[li] = {} 74 | end 75 | return setmetatable(t, {__index = m}) 76 | end 77 | return setmetatable(f, {__call = f.new}) 78 | -------------------------------------------------------------------------------- /link.agpack/src/Directories.lua: -------------------------------------------------------------------------------- 1 | -- Directory Parser — Copyright 2021 Scott Smith -- 2 | 3 | local setmetatable = setmetatable 4 | local sfmt = string.format 5 | local sfind = string.find 6 | local tins = table.insert 7 | local tcat = table.concat 8 | 9 | local m = {} 10 | 11 | local function tree_to_path(tree, limit) 12 | local path 13 | local idx = 1 14 | if tree.absolute and tree.volume then 15 | idx = 0 16 | end 17 | limit = limit or #tree 18 | if limit < 0 then 19 | print("LIMIT!", limit) 20 | limit = #tree + limit 21 | end 22 | return tcat(tree, tree.separator, idx, limit) 23 | end 24 | 25 | --system() runs over a files table updating the trees. 26 | function m.update(files) 27 | --TODO:test on Linux, for now, I can only assume it'll work 28 | for i = 1, #files, 2 do 29 | local name = files[i] 30 | local path = files[i+1] 31 | files[i] = nil 32 | files[i+1] = nil 33 | local tree = setmetatable({}, {__tostring = tree_to_path}) 34 | tree.topath = tree_to_path 35 | 36 | local save_e = 0 37 | 38 | local path_sep 39 | local find_path_sep 40 | local s, e, volume 41 | do 42 | if sfind(path, "/") then 43 | path_sep = "/" 44 | elseif sfind(path, "\\") then 45 | path_sep = "\\" 46 | else 47 | --just a single file (same directory as base file) 48 | tree.volume = false 49 | tree.relative = true 50 | tree[1] = path 51 | goto nopath 52 | end 53 | end 54 | find_path_sep = "^"..path_sep 55 | tree.separator = path_sep 56 | 57 | s, e, volume = sfind(path, "^(%a):") 58 | if volume then 59 | save_e = e 60 | elseif path_sep == "\\" and sfind(path, "^\\\\") then 61 | volume = "\\" 62 | e = 1 63 | save_e = e 64 | else 65 | volume = false 66 | e = save_e 67 | end 68 | s, e = sfind(path, find_path_sep, e+1) 69 | if s then 70 | tree.absolute = true 71 | if volume then 72 | local v = volume 73 | if v ~= "\\" then 74 | v = v..":" 75 | end 76 | tree[0] = v 77 | end 78 | else 79 | tree.relative = true 80 | e = save_e 81 | volume = false 82 | end 83 | tree.volume = volume 84 | do 85 | local find_w_sep = "^([^"..path_sep.."]+)" 86 | while e and e <= #path do 87 | local dir 88 | s, e, dir = sfind(path, find_w_sep, e + 1) 89 | if dir then 90 | tins(tree, dir) 91 | s, e = sfind(path, path_sep, e + 1) 92 | end 93 | end 94 | end 95 | ::nopath:: 96 | --print(sfmt("%20s(%s), %s:> %s", name, tree.absolute and "ABS" or "REL", 97 | -- tree.volume or " ", 98 | -- table.concat(tree, " > ") 99 | --)) 100 | 101 | tins(files.names, name) 102 | files.paths[name] = path 103 | files.trees[name] = tree 104 | 105 | end 106 | return files 107 | end 108 | 109 | local function get_relative(self, p1n, p2n) 110 | local p1 = self.trees[p1n] or self:enqueue(p1n) 111 | local p2 = self.trees[p2n] or self:enqueue(p2n) 112 | if #self > 0 then 113 | self:update() 114 | p1 = self.trees[p1n] 115 | p2 = self.trees[p2n] 116 | end 117 | 118 | if not p1.absolute then 119 | return false, "Path of argument 1 must be absolute" 120 | end 121 | local reltree 122 | if p2.absolute and p1.volume == p2.volume then 123 | local min = #p1 < #p2 and #p1 or #p2 124 | local base_i = min 125 | for i = 1, min do 126 | if p1[i] ~= p2[i] then 127 | base_i = i 128 | break 129 | end 130 | end 131 | 132 | local parent_dirs = #p1 - base_i 133 | 134 | --print(sfmt("%2d %17s %s:> %s", #p1 - base_i, "p1", p1.volume, table.concat(p1, " > ", base_i))) 135 | --print(sfmt("%2d %17s %s:> %s", #p2 - base_i, "p2", p2.volume, table.concat(p2, " > ", base_i))) 136 | 137 | reltree = setmetatable({}, {__tostring = tree_to_path}) 138 | reltree.topath = tree_to_path 139 | --relative directories must be on same volume 140 | reltree.volume = false 141 | reltree.relative = true 142 | reltree.separator = p1.separator 143 | for i = 1, #p1 - base_i do 144 | tins(reltree, "..") 145 | end 146 | for i = base_i, #p2 do 147 | tins(reltree, p2[i]) 148 | end 149 | else 150 | reltree = p2 151 | end 152 | return reltree 153 | end 154 | 155 | local function to_absolute(self, p1n, p2n) 156 | local p1 = self.trees[p1n] or self:enqueue(p1n) 157 | local p2 = self.trees[p2n] or self:enqueue(p2n) 158 | if #self > 0 then 159 | self:update() 160 | p1 = self.trees[p1n] 161 | p2 = self.trees[p2n] 162 | end 163 | if p2.absolute then 164 | return p2 165 | elseif not p1.absolute then 166 | return false, "Path of argument 1 must be absolute" 167 | end 168 | local abstree = setmetatable({}, {__tostring = tree_to_path}) 169 | abstree.topath = tree_to_path 170 | do 171 | local volume = p1.volume 172 | if volume then 173 | abstree.volume = volume 174 | abstree[0] = p1[0] 175 | end 176 | end 177 | abstree.absolute = true 178 | abstree.separator = p1.separator 179 | 180 | local c = 0 181 | for i = 1, #p1 - 1 do --drop last part of path (assuming it's a file) 182 | c = c + 1 183 | abstree[c] = p1[i] 184 | end 185 | 186 | for i = 1, #p2 do 187 | if p2[i] == ".." then 188 | c = c - 1 189 | else 190 | c = c + 1 191 | abstree[c] = p2[i] 192 | end 193 | end 194 | for i = c+1, #abstree do 195 | abstree[i] = nil 196 | end 197 | return abstree 198 | end 199 | 200 | --enqueue(name, path) 201 | --enqueue(path) 202 | function m:enqueue(name, path) 203 | path = path or name 204 | 205 | tins(self, name) 206 | tins(self, path) 207 | end 208 | 209 | function m:add(name, tree) 210 | self.trees[name] = tree 211 | end 212 | 213 | local function new(_) 214 | local files = {} 215 | files.names = {} 216 | files.paths = {} 217 | files.trees = {} 218 | 219 | files.get_relative = get_relative 220 | files.to_absolute = to_absolute 221 | 222 | return setmetatable(files, {__index = m}) 223 | end 224 | 225 | return setmetatable({}, {__call = new}) 226 | -------------------------------------------------------------------------------- /link.agpack/src/Entity.lua: -------------------------------------------------------------------------------- 1 | --Copyright 2020 -- Scott Smith -- 2 | 3 | local setmetatable = setmetatable 4 | local type, select 5 | = type, select 6 | 7 | local m = {} 8 | 9 | function m:new() local list = self 10 | local last = self.last 11 | local max = self.max 12 | 13 | local next_entity_idx 14 | for i = last + 1, max do 15 | if not list[i] or list[i] == 0 then 16 | next_entity_idx = i 17 | break 18 | end 19 | end 20 | if not next_entity_idx then 21 | next_entity_idx = max + 1 22 | self.max = max + 1 23 | end 24 | list[next_entity_idx] = true 25 | 26 | self.last = next_entity_idx 27 | 28 | return next_entity_idx 29 | end 30 | 31 | function m:add_existing(...) local list = self 32 | local arr_max = 0 33 | local length 34 | local select = select 35 | local arr = select(1, ...) 36 | if type(arr) == "table" then 37 | length = #arr 38 | select = function (idx) return arr[idx] end 39 | elseif type(arr) == "number" then 40 | length = select("#", ...) 41 | else 42 | --This isn't enforced once the array is processed, but I don't care. 43 | --Entities can be other types, but with undefined behavior. 44 | return nil, "invalid type, must pass number or table" 45 | end 46 | 47 | for i = 1, length do 48 | local eid = select(i, ...) 49 | if eid > arr_max then 50 | arr_max = eid 51 | end 52 | list[eid] = true 53 | end 54 | 55 | if arr_max > list.max then 56 | list.max = arr_max 57 | end 58 | return true 59 | end 60 | 61 | --convert an existing component's ids to newly generated ids 62 | --great for reusing generic components 63 | function m:convert(component, new) 64 | local eid = component.eid 65 | local pid = component.pid 66 | if not eid or not pid then 67 | return nil, "invalid component, missing eid or pid" 68 | end 69 | if not new or new.type ~= "component" or not new.eid or not new.pid then 70 | return nil, "invalid new component, unable to generate ids" 71 | end 72 | local neid = new.eid 73 | local npid = new.pid 74 | for i = 1, #eid do 75 | npid[i] = 0 76 | end 77 | for i = 1, #eid do 78 | local nid = self:new() 79 | neid[i] = nid 80 | for k = 1, #eid do 81 | if pid[k] == eid[i] then 82 | npid[k] = nid 83 | end 84 | end 85 | end 86 | --make sure neid table is same length as eid table 87 | for i = #eid + 1, #neid do 88 | neid[i] = nil 89 | end 90 | 91 | local zero 92 | for i = 1, #npid do 93 | if npid[i] == 0 then 94 | if zero then 95 | return nil, "only one entity with parent id equal to 0 allowed" 96 | end 97 | zero = true 98 | end 99 | end 100 | return new 101 | end 102 | 103 | function m:remove() 104 | error("not implimented") 105 | end 106 | 107 | local f = {} 108 | function f.new() 109 | local entity_list = {} 110 | entity_list.last = 0 111 | entity_list.max = 0 112 | return setmetatable(entity_list, {__index = m}) 113 | end 114 | 115 | return setmetatable(f, {__call = f.new}) 116 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | -- Copywrite 2020 -- Scott Smith -- 2 | 3 | --TODO: switch from package.path to love.filesystem.setRequirePath 4 | local engine_src_dir = "./data" 5 | package.path = "./data/?.lua;./data/?/init.lua;" 6 | local tracestrip = "^%s*%./data/" 7 | local thisfilestrip = "^%s*main%.lua:" 8 | 9 | DEBUG = true 10 | 11 | --main2: true, load once; false, load on each reset 12 | --TODO:setting main2, maybe the logic should be switched... 13 | local main2 = not hal_conf.reload_main2_on_restart 14 | 15 | local require = require 16 | package.path = package.path .. "/usr/local/share/lua/5.1/?.lua;" 17 | if hal_conf.websocket_enabled then -- obs remote control 18 | local ev = require "ev" 19 | require "socket" 20 | require "ltn12" 21 | require "mime" 22 | 23 | hal_client = require "websocket.client".ev() 24 | 25 | hal_client:on_open(function() print("connected") end) 26 | hal_client:on_message(function(ws, msg) 27 | print("received", msg) 28 | end) 29 | hal_client:connect(hal_conf.ws_url, hal_conf.ws_protocol) 30 | ev.Idle.new(function(loop) loop:unloop() end):start(ev.Loop.default) 31 | ev.Loop.default:loop() 32 | end 33 | if hal_conf.midi and love.system.getOS() == "Linux" then 34 | local alsa = require "midialsa" 35 | alsa.client("Akciom Engine", 1, 1) 36 | alsa.connectfrom(0, hal_conf.midi_control) 37 | alsa.connectto(1, hal_conf.midi_control) 38 | halmidi_saved = {} 39 | end 40 | 41 | --global of all ffi.cdefs and if they have been initialized 42 | --A ctype/metatype can only be created once 43 | hal_defined = {} 44 | 45 | --{{{ [[ GLOBAL VARIABLES printf, printftime, log ]] 46 | local export_hal = { debug = {} } 47 | printf = require "utils.printf" 48 | printftime = require "utils.logging".time 49 | log = require"utils.logging"({ hal = export_hal }) 50 | local printf, printftime = printf, printftime 51 | --}}} 52 | 53 | ---{{{ [[ local variable declaration ]] 54 | local assert, next, xpcall = assert, next, xpcall 55 | local table, debug, string = table, debug, string 56 | local collectgarbage = collectgarbage 57 | local love = love 58 | local lg = love.graphics 59 | local reloadkind = false 60 | --}}} 61 | 62 | do 63 | local global_saved = {} for k,v in next, _G do global_saved[k] = v end 64 | require"strict"("export", _G) 65 | 66 | if main2 and type(main2) == "boolean" then 67 | print("** only .agpack and include files are reloaded") 68 | reloadkind = ".agpack" 69 | main2 = require"main2" 70 | global_saved.hal = hal 71 | global_saved.keys2 = keys2 72 | else 73 | print("** main2 will be reloaded") 74 | reloadkind = "main2" 75 | end 76 | 77 | --{{{ [[ love.handlers: reload/quit ]] 78 | 79 | local package_loaded_saved = {} 80 | for k,v in next, package.loaded do 81 | package_loaded_saved[k] = v 82 | end 83 | 84 | function love.handlers.reload(donoterror) 85 | printftime("Reloading %s...", reloadkind) 86 | 87 | printf("(%dKB -->", collectgarbage("count")) 88 | for i = 1, 10 do 89 | collectgarbage() 90 | end 91 | collectgarbage("stop") 92 | printf(" %dKB)", collectgarbage("count")) 93 | collectgarbage("stop") 94 | 95 | export("clear", "all") 96 | for k,v in next, global_saved do 97 | export[k] = v 98 | end 99 | 100 | local pl = package.loaded 101 | for k,v in next, pl do 102 | pl[k] = package_loaded_saved[k] 103 | end 104 | print("done!\n --------------------") 105 | if donoterror == "donoterror" then 106 | --was likely called explicitly, probably from this file 107 | return true 108 | else 109 | --was likely generated by an event in the main game. 110 | error("RELOAD_THE_GAME_PLEASE") 111 | end 112 | end 113 | 114 | function love.handlers.quit() 115 | if love.audio then 116 | love.audio.stop() 117 | end 118 | 119 | if not love.quit or love.quit() then 120 | error("QUIT_THE_GAME_PLEASE") 121 | end 122 | end 123 | --}}} 124 | end 125 | 126 | --{{{ [[ Custom Error Handling ]] 127 | local ERROR_IN_ERROR_HANDLING = "error in error handling" 128 | local myerror do 129 | local errfont 130 | local function set_errfont() 131 | errfont = lg.getFont() 132 | local success, font = pcall( 133 | lg.setNewFont, "assets/DejaVuSansMono.ttf", 11 134 | ) 135 | if success then 136 | errfont = font 137 | end 138 | end 139 | if love.window.isOpen() then 140 | set_errfont() 141 | end 142 | 143 | function myerror(message, level) 144 | --Special error string matching 145 | if message == ERROR_IN_ERROR_HANDLING then print("** "..message.." **")end 146 | if message:match("RELOAD_THE_GAME_PLEASE$") then return true end 147 | if message:match("QUIT_THE_GAME_PLEASE$") then return false end 148 | --reset state (from love.errhand) 149 | if love.mouse then 150 | love.mouse.setVisible(true) 151 | love.mouse.setGrabbed(false) 152 | end 153 | if love.joystick then 154 | for i,v in ipairs(love.joystick.getJoysticks()) do 155 | v:setVibration() 156 | end 157 | end 158 | if love.audio then love.audio.stop() end 159 | 160 | if love.window and love.window.isOpen() then 161 | if not errfont then set_errfont() end 162 | lg.setFont(errfont) 163 | lg.reset() 164 | lg.setBackgroundColor(0x40/0xff, 0x80/0xff, 0xa0/0xff) 165 | lg.setColor(1.0, 1.0, 1.0, 1.0) 166 | lg.origin() 167 | end 168 | --end:reset state 169 | 170 | --debug information 171 | level = (level or 1) + 2 172 | local info = debug.getinfo(level, "Sl") 173 | local trace = debug.traceback("", level) 174 | --end:debug information 175 | 176 | local tinsert, tconcat = table.insert, table.concat 177 | local pt = {} 178 | if message:match("HALT_THE_GAME_PLEASE$") then 179 | tinsert(pt, "Halting Program") 180 | else 181 | tinsert(pt, "Error\n") 182 | if message:match("_PLEASE$") then 183 | tinsert(pt, "Unrecognized _PLEASE command\n") 184 | end 185 | 186 | message = string.gsub(message, tracestrip, "") 187 | tinsert(pt, message) 188 | 189 | --The string matching here is dependent on source files being in src 190 | --see local variables tracestrip and thisfilestrip 191 | local levels = level-1 192 | for l in string.gmatch(trace, "(.-)\n") do 193 | levels = levels + 1 194 | if not string.match(l, thisfilestrip) then 195 | l = string.gsub(l, tracestrip, "\t") 196 | tinsert(pt, l) 197 | else 198 | table.remove(pt) --remove last xpcall reference 199 | break 200 | end 201 | end 202 | --end:trace line stripping 203 | end 204 | 205 | tinsert(pt, "\n(R)eload (Esc), (Q)uit, (D)ebug") 206 | local pts = tconcat(pt, "\n") 207 | print(pts) 208 | 209 | local stop, rewind = false, false 210 | local quit_timer = false 211 | local debugging = false 212 | 213 | if not export("check", "keys2") then 214 | for k,v in next, export_hal do 215 | export_hal[k] = nil 216 | end 217 | export. hal = export_hal 218 | export. keys2 = dofile(engine_src_dir.."/keys2.lua") 219 | end 220 | local keys2handlers = keys2.getHandlers() 221 | keys2handlers.quit = function() stop = true end 222 | --squelch handlers not used 223 | keys2handlers.visible = false 224 | keys2handlers.mousefocus = false 225 | keys2handlers.focus = false 226 | 227 | local keys = keys2.keys 228 | local midi = keys2.midi 229 | 230 | 231 | local dt = 1/20 232 | while true do 233 | keys2.doevents() 234 | love.event.pump() 235 | for ev,a,b,c,d,e,f in love.event.poll() do 236 | if keys2handlers[ev] then 237 | keys2handlers[ev](a,b,c,d,e,f) 238 | elseif keys2handlers[ev] == nil then 239 | printf("event %s not recognized", ev) 240 | end 241 | end 242 | 243 | for i = 1, keys2.pressed.count do 244 | local kp = keys2.pressed[i] 245 | if kp == "escape" then 246 | quit_timer = 0 247 | elseif kp == "q" then 248 | stop = true 249 | elseif kp == "r" then 250 | rewind = true 251 | elseif kp == "d" then 252 | tinsert(pt, "- debugging...") 253 | pts = tconcat(pt, "\n") 254 | debugging = true 255 | end 256 | end 257 | for i = 1, keys2.released.count do 258 | local kr = keys2.released[i] 259 | if kr == "escape" then 260 | rewind = true 261 | end 262 | end 263 | 264 | if quit_timer then 265 | quit_timer = quit_timer + dt 266 | if quit_timer > 0.3228 then stop = true end 267 | end 268 | if midi(0xb02a) > 0.5 then 269 | if not midi.saved._hashalted then stop = true end 270 | else 271 | midi.saved._hashalted = false 272 | end 273 | 274 | if stop then 275 | return false 276 | end 277 | if rewind or midi(0xb02b) > 0.5 then 278 | midi.saved[0xb02b] = 0 --so it doesn't reset in escapekey() 279 | return love.handlers.reload("donoterror") 280 | end 281 | keys2:reset() 282 | 283 | --Render 284 | lg.clear(lg.getBackgroundColor()) 285 | lg.print(pts, 10, 10)--, lg.getWidth() - 10) 286 | lg.present() 287 | --end:Render 288 | 289 | if debugging then 290 | debug.debug() 291 | local ptlen = #pt 292 | pt[ptlen] = pt[ptlen].."done" 293 | pts = table.concat(pt, "\n") 294 | debugging = false 295 | end 296 | 297 | if love.timer then love.timer.sleep(dt) end 298 | end 299 | end 300 | end --}}} end:custom error handling 301 | 302 | 303 | function love.run() 304 | local keepgameopen = false 305 | if arg[2] == "keepgameopen" then 306 | table.remove(arg, 2) 307 | keepgameopen = true 308 | end 309 | 310 | local error_retries = 0 311 | while true do 312 | --loading 313 | if love.window.isOpen() then 314 | lg.clear() --clear whatever was in gfx buffer before loading 315 | lg.present() 316 | end 317 | collectgarbage() 318 | printftime("Starting Program...(%dKB)\n", collectgarbage("count")) 319 | collectgarbage("stop") 320 | --end:loading 321 | 322 | local clean, continue = xpcall(function() 323 | if main2 then 324 | return main2(arg) 325 | else 326 | return require"main2"(arg) 327 | end 328 | end, myerror) 329 | 330 | --reload if clean and continue is boolean true 331 | if clean then break end 332 | if not continue then break end 333 | if type(continue) == "string" then 334 | if continue == ERROR_IN_ERROR_HANDLING then 335 | myerror(continue) 336 | end 337 | print(" **********************************************") 338 | print(" ************************************************") 339 | print("**************************************************") 340 | print("*+----------------------------------------------/ ") 341 | print("*\\ ") 342 | print("** Error: \""..continue.."\", ffi is possible culprit") 343 | print("** Retrying without using custom error handling") 344 | print("*/ ") 345 | print("*+----------------------------------------------\\ ") 346 | print("**************************************************") 347 | print(" ************************************************") 348 | print(" **********************************************") 349 | io.flush() 350 | love.handlers.reload("donoterror") 351 | love.timer.sleep(.50) 352 | if error_retries > 0 then 353 | print("Retrying once more using custom error handling.") 354 | error_retries = error_retries + 1 355 | else 356 | if main2 then 357 | main2() 358 | else 359 | require"main2"(arg) 360 | end 361 | break 362 | end 363 | end 364 | end 365 | printf("Thank you for playing.\n") 366 | 367 | if keepgameopen then 368 | printf("Press ENTER to close...") 369 | love.window.setDisplaySleepEnabled(true) 370 | love.window.close() 371 | io.read() 372 | end 373 | end 374 | --------------------------------------------------------------------------------