├── 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 |
--------------------------------------------------------------------------------