feat: remove libs
This commit is contained in:
parent
66b77d59ed
commit
bc8d61728b
18 changed files with 0 additions and 4687 deletions
|
@ -1,687 +0,0 @@
|
|||
-- binser.lua
|
||||
|
||||
--[[
|
||||
Copyright (c) 2016 Calvin Rose
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
|
||||
local assert = assert
|
||||
local error = error
|
||||
local select = select
|
||||
local pairs = pairs
|
||||
local getmetatable = getmetatable
|
||||
local setmetatable = setmetatable
|
||||
local tonumber = tonumber
|
||||
local type = type
|
||||
local loadstring = loadstring or load
|
||||
local concat = table.concat
|
||||
local char = string.char
|
||||
local byte = string.byte
|
||||
local format = string.format
|
||||
local sub = string.sub
|
||||
local dump = string.dump
|
||||
local floor = math.floor
|
||||
local frexp = math.frexp
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
-- Lua 5.3 frexp polyfill
|
||||
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
|
||||
if not frexp then
|
||||
local log, abs, floor = math.log, math.abs, math.floor
|
||||
local log2 = log(2)
|
||||
frexp = function(x)
|
||||
if x == 0 then return 0, 0 end
|
||||
local e = floor(log(abs(x)) / log2 + 1)
|
||||
return x / 2 ^ e, e
|
||||
end
|
||||
end
|
||||
|
||||
-- NIL = 202
|
||||
-- FLOAT = 203
|
||||
-- TRUE = 204
|
||||
-- FALSE = 205
|
||||
-- STRING = 206
|
||||
-- TABLE = 207
|
||||
-- REFERENCE = 208
|
||||
-- CONSTRUCTOR = 209
|
||||
-- FUNCTION = 210
|
||||
-- RESOURCE = 211
|
||||
-- INT64 = 212
|
||||
|
||||
local mts = {}
|
||||
local ids = {}
|
||||
local serializers = {}
|
||||
local deserializers = {}
|
||||
local resources = {}
|
||||
local resources_by_name = {}
|
||||
|
||||
local function pack(...)
|
||||
return {...}, select("#", ...)
|
||||
end
|
||||
|
||||
local function not_array_index(x, len)
|
||||
return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
|
||||
end
|
||||
|
||||
local function type_check(x, tp, name)
|
||||
assert(type(x) == tp,
|
||||
format("Expected parameter %q to be of type %q.", name, tp))
|
||||
end
|
||||
|
||||
local bigIntSupport = false
|
||||
local isInteger
|
||||
if math.type then -- Detect Lua 5.3
|
||||
local mtype = math.type
|
||||
bigIntSupport = loadstring[[
|
||||
local char = string.char
|
||||
return function(n)
|
||||
local nn = n < 0 and -(n + 1) or n
|
||||
local b1 = nn // 0x100000000000000
|
||||
local b2 = nn // 0x1000000000000 % 0x100
|
||||
local b3 = nn // 0x10000000000 % 0x100
|
||||
local b4 = nn // 0x100000000 % 0x100
|
||||
local b5 = nn // 0x1000000 % 0x100
|
||||
local b6 = nn // 0x10000 % 0x100
|
||||
local b7 = nn // 0x100 % 0x100
|
||||
local b8 = nn % 0x100
|
||||
if n < 0 then
|
||||
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
|
||||
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
|
||||
end
|
||||
return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
|
||||
end]]()
|
||||
isInteger = function(x)
|
||||
return mtype(x) == 'integer'
|
||||
end
|
||||
else
|
||||
isInteger = function(x)
|
||||
return floor(x) == x
|
||||
end
|
||||
end
|
||||
|
||||
-- Copyright (C) 2012-2015 Francois Perrad.
|
||||
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
|
||||
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
|
||||
local function number_to_str(n)
|
||||
if isInteger(n) then -- int
|
||||
if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
|
||||
return char(n + 27)
|
||||
elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
|
||||
n = n + 8192
|
||||
return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
|
||||
elseif bigIntSupport then
|
||||
return bigIntSupport(n)
|
||||
end
|
||||
end
|
||||
local sign = 0
|
||||
if n < 0.0 then
|
||||
sign = 0x80
|
||||
n = -n
|
||||
end
|
||||
local m, e = frexp(n) -- mantissa, exponent
|
||||
if m ~= m then
|
||||
return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
elseif m == 1/0 then
|
||||
if sign == 0 then
|
||||
return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
else
|
||||
return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
end
|
||||
end
|
||||
e = e + 0x3FE
|
||||
if e < 1 then -- denormalized numbers
|
||||
m = m * 2 ^ (52 + e)
|
||||
e = 0
|
||||
else
|
||||
m = (m * 2 - 1) * 2 ^ 52
|
||||
end
|
||||
return char(203,
|
||||
sign + floor(e / 0x10),
|
||||
(e % 0x10) * 0x10 + floor(m / 0x1000000000000),
|
||||
floor(m / 0x10000000000) % 0x100,
|
||||
floor(m / 0x100000000) % 0x100,
|
||||
floor(m / 0x1000000) % 0x100,
|
||||
floor(m / 0x10000) % 0x100,
|
||||
floor(m / 0x100) % 0x100,
|
||||
m % 0x100)
|
||||
end
|
||||
|
||||
-- Copyright (C) 2012-2015 Francois Perrad.
|
||||
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
|
||||
local function number_from_str(str, index)
|
||||
local b = byte(str, index)
|
||||
if b < 128 then
|
||||
return b - 27, index + 1
|
||||
elseif b < 192 then
|
||||
return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2
|
||||
end
|
||||
local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
|
||||
if b == 212 then
|
||||
local flip = b1 >= 128
|
||||
if flip then -- negative
|
||||
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
|
||||
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
|
||||
end
|
||||
local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
|
||||
if flip then
|
||||
return (-n) - 1, index + 9
|
||||
else
|
||||
return n, index + 9
|
||||
end
|
||||
end
|
||||
local sign = b1 > 0x7F and -1 or 1
|
||||
local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
|
||||
local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
|
||||
local n
|
||||
if e == 0 then
|
||||
if m == 0 then
|
||||
n = sign * 0.0
|
||||
else
|
||||
n = sign * (m / 2 ^ 52) * 2 ^ -1022
|
||||
end
|
||||
elseif e == 0x7FF then
|
||||
if m == 0 then
|
||||
n = sign * (1/0)
|
||||
else
|
||||
n = 0.0/0.0
|
||||
end
|
||||
else
|
||||
n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
|
||||
end
|
||||
return n, index + 9
|
||||
end
|
||||
|
||||
local types = {}
|
||||
|
||||
types["nil"] = function(x, visited, accum)
|
||||
accum[#accum + 1] = "\202"
|
||||
end
|
||||
|
||||
function types.number(x, visited, accum)
|
||||
accum[#accum + 1] = number_to_str(x)
|
||||
end
|
||||
|
||||
function types.boolean(x, visited, accum)
|
||||
accum[#accum + 1] = x and "\204" or "\205"
|
||||
end
|
||||
|
||||
function types.string(x, visited, accum)
|
||||
local alen = #accum
|
||||
if visited[x] then
|
||||
accum[alen + 1] = "\208"
|
||||
accum[alen + 2] = number_to_str(visited[x])
|
||||
else
|
||||
visited[x] = visited.next
|
||||
visited.next = visited.next + 1
|
||||
accum[alen + 1] = "\206"
|
||||
accum[alen + 2] = number_to_str(#x)
|
||||
accum[alen + 3] = x
|
||||
end
|
||||
end
|
||||
|
||||
local function check_custom_type(x, visited, accum)
|
||||
local res = resources[x]
|
||||
if res then
|
||||
accum[#accum + 1] = "\211"
|
||||
types[type(res)](res, visited, accum)
|
||||
return true
|
||||
end
|
||||
local mt = getmetatable(x)
|
||||
local id = mt and ids[mt]
|
||||
if id then
|
||||
if x == visited.temp then
|
||||
error("Infinite loop in constructor.")
|
||||
end
|
||||
visited.temp = x
|
||||
accum[#accum + 1] = "\209"
|
||||
types[type(id)](id, visited, accum)
|
||||
local args, len = pack(serializers[id](x))
|
||||
accum[#accum + 1] = number_to_str(len)
|
||||
for i = 1, len do
|
||||
local arg = args[i]
|
||||
types[type(arg)](arg, visited, accum)
|
||||
end
|
||||
visited[x] = visited.next
|
||||
visited.next = visited.next + 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function types.userdata(x, visited, accum)
|
||||
if visited[x] then
|
||||
accum[#accum + 1] = "\208"
|
||||
accum[#accum + 1] = number_to_str(visited[x])
|
||||
else
|
||||
if check_custom_type(x, visited, accum) then return end
|
||||
error("Cannot serialize this userdata.")
|
||||
end
|
||||
end
|
||||
|
||||
function types.table(x, visited, accum)
|
||||
if visited[x] then
|
||||
accum[#accum + 1] = "\208"
|
||||
accum[#accum + 1] = number_to_str(visited[x])
|
||||
else
|
||||
if check_custom_type(x, visited, accum) then return end
|
||||
visited[x] = visited.next
|
||||
visited.next = visited.next + 1
|
||||
local xlen = #x
|
||||
accum[#accum + 1] = "\207"
|
||||
accum[#accum + 1] = number_to_str(xlen)
|
||||
for i = 1, xlen do
|
||||
local v = x[i]
|
||||
types[type(v)](v, visited, accum)
|
||||
end
|
||||
local key_count = 0
|
||||
for k in pairs(x) do
|
||||
if not_array_index(k, xlen) then
|
||||
key_count = key_count + 1
|
||||
end
|
||||
end
|
||||
accum[#accum + 1] = number_to_str(key_count)
|
||||
for k, v in pairs(x) do
|
||||
if not_array_index(k, xlen) then
|
||||
types[type(k)](k, visited, accum)
|
||||
types[type(v)](v, visited, accum)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
types["function"] = function(x, visited, accum)
|
||||
if visited[x] then
|
||||
accum[#accum + 1] = "\208"
|
||||
accum[#accum + 1] = number_to_str(visited[x])
|
||||
else
|
||||
if check_custom_type(x, visited, accum) then return end
|
||||
visited[x] = visited.next
|
||||
visited.next = visited.next + 1
|
||||
local str = dump(x)
|
||||
accum[#accum + 1] = "\210"
|
||||
accum[#accum + 1] = number_to_str(#str)
|
||||
accum[#accum + 1] = str
|
||||
end
|
||||
end
|
||||
|
||||
types.cdata = function(x, visited, accum)
|
||||
if visited[x] then
|
||||
accum[#accum + 1] = "\208"
|
||||
accum[#accum + 1] = number_to_str(visited[x])
|
||||
else
|
||||
if check_custom_type(x, visited, #accum) then return end
|
||||
error("Cannot serialize this cdata.")
|
||||
end
|
||||
end
|
||||
|
||||
types.thread = function() error("Cannot serialize threads.") end
|
||||
|
||||
local function deserialize_value(str, index, visited)
|
||||
local t = byte(str, index)
|
||||
if not t then return end
|
||||
if t < 128 then
|
||||
return t - 27, index + 1
|
||||
elseif t < 192 then
|
||||
return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2
|
||||
elseif t == 202 then
|
||||
return nil, index + 1
|
||||
elseif t == 203 then
|
||||
return number_from_str(str, index)
|
||||
elseif t == 204 then
|
||||
return true, index + 1
|
||||
elseif t == 205 then
|
||||
return false, index + 1
|
||||
elseif t == 206 then
|
||||
local length, dataindex = deserialize_value(str, index + 1, visited)
|
||||
local nextindex = dataindex + length
|
||||
local substr = sub(str, dataindex, nextindex - 1)
|
||||
visited[#visited + 1] = substr
|
||||
return substr, nextindex
|
||||
elseif t == 207 then
|
||||
local count, nextindex = number_from_str(str, index + 1)
|
||||
local ret = {}
|
||||
visited[#visited + 1] = ret
|
||||
for i = 1, count do
|
||||
ret[i], nextindex = deserialize_value(str, nextindex, visited)
|
||||
end
|
||||
count, nextindex = number_from_str(str, nextindex)
|
||||
for i = 1, count do
|
||||
local k, v
|
||||
k, nextindex = deserialize_value(str, nextindex, visited)
|
||||
v, nextindex = deserialize_value(str, nextindex, visited)
|
||||
ret[k] = v
|
||||
end
|
||||
return ret, nextindex
|
||||
elseif t == 208 then
|
||||
local ref, nextindex = number_from_str(str, index + 1)
|
||||
return visited[ref], nextindex
|
||||
elseif t == 209 then
|
||||
local count
|
||||
local name, nextindex = deserialize_value(str, index + 1, visited)
|
||||
count, nextindex = number_from_str(str, nextindex)
|
||||
local args = {}
|
||||
for i = 1, count do
|
||||
args[i], nextindex = deserialize_value(str, nextindex, visited)
|
||||
end
|
||||
local ret = deserializers[name](unpack(args))
|
||||
visited[#visited + 1] = ret
|
||||
return ret, nextindex
|
||||
elseif t == 210 then
|
||||
local length, dataindex = deserialize_value(str, index + 1, visited)
|
||||
local nextindex = dataindex + length
|
||||
local ret = loadstring(sub(str, dataindex, nextindex - 1))
|
||||
visited[#visited + 1] = ret
|
||||
return ret, nextindex
|
||||
elseif t == 211 then
|
||||
local res, nextindex = deserialize_value(str, index + 1, visited)
|
||||
return resources_by_name[res], nextindex
|
||||
elseif t == 212 then
|
||||
return number_from_str(str, index)
|
||||
else
|
||||
error("Could not deserialize type byte " .. t .. ".")
|
||||
end
|
||||
end
|
||||
|
||||
local function serialize(...)
|
||||
local visited = {next = 1}
|
||||
local accum = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local x = select(i, ...)
|
||||
types[type(x)](x, visited, accum)
|
||||
end
|
||||
return concat(accum)
|
||||
end
|
||||
|
||||
local function make_file_writer(file)
|
||||
return setmetatable({}, {
|
||||
__newindex = function(_, _, v)
|
||||
file:write(v)
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
local function serialize_to_file(path, mode, ...)
|
||||
local file, err = io.open(path, mode)
|
||||
assert(file, err)
|
||||
local visited = {next = 1}
|
||||
local accum = make_file_writer(file)
|
||||
for i = 1, select("#", ...) do
|
||||
local x = select(i, ...)
|
||||
types[type(x)](x, visited, accum)
|
||||
end
|
||||
-- flush the writer
|
||||
file:flush()
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function writeFile(path, ...)
|
||||
return serialize_to_file(path, "wb", ...)
|
||||
end
|
||||
|
||||
local function appendFile(path, ...)
|
||||
return serialize_to_file(path, "ab", ...)
|
||||
end
|
||||
|
||||
local function deserialize(str, index)
|
||||
assert(type(str) == "string", "Expected string to deserialize.")
|
||||
local vals = {}
|
||||
index = index or 1
|
||||
local visited = {}
|
||||
local len = 0
|
||||
local val
|
||||
while index do
|
||||
val, index = deserialize_value(str, index, visited)
|
||||
if index then
|
||||
len = len + 1
|
||||
vals[len] = val
|
||||
end
|
||||
end
|
||||
return vals, len
|
||||
end
|
||||
|
||||
local function deserializeN(str, n, index)
|
||||
assert(type(str) == "string", "Expected string to deserialize.")
|
||||
n = n or 1
|
||||
assert(type(n) == "number", "Expected a number for parameter n.")
|
||||
assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
|
||||
local vals = {}
|
||||
index = index or 1
|
||||
local visited = {}
|
||||
local len = 0
|
||||
local val
|
||||
while index and len < n do
|
||||
val, index = deserialize_value(str, index, visited)
|
||||
if index then
|
||||
len = len + 1
|
||||
vals[len] = val
|
||||
end
|
||||
end
|
||||
vals[len + 1] = index
|
||||
return unpack(vals, 1, n + 1)
|
||||
end
|
||||
|
||||
local function readFile(path)
|
||||
local file, err = io.open(path, "rb")
|
||||
assert(file, err)
|
||||
local str = file:read("*all")
|
||||
file:close()
|
||||
return deserialize(str)
|
||||
end
|
||||
|
||||
local function default_deserialize(metatable)
|
||||
return function(...)
|
||||
local ret = {}
|
||||
for i = 1, select("#", ...), 2 do
|
||||
ret[select(i, ...)] = select(i + 1, ...)
|
||||
end
|
||||
return setmetatable(ret, metatable)
|
||||
end
|
||||
end
|
||||
|
||||
local function default_serialize(x)
|
||||
assert(type(x) == "table",
|
||||
"Default serialization for custom types only works for tables.")
|
||||
local args = {}
|
||||
local len = 0
|
||||
for k, v in pairs(x) do
|
||||
args[len + 1], args[len + 2] = k, v
|
||||
len = len + 2
|
||||
end
|
||||
return unpack(args, 1, len)
|
||||
end
|
||||
|
||||
-- Templating
|
||||
|
||||
local function normalize_template(template)
|
||||
local ret = {}
|
||||
for i = 1, #template do
|
||||
ret[i] = template[i]
|
||||
end
|
||||
local non_array_part = {}
|
||||
-- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
|
||||
-- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
|
||||
-- in templates. Looking for way around this.
|
||||
for k in pairs(template) do
|
||||
if not_array_index(k, #template) then
|
||||
non_array_part[#non_array_part + 1] = k
|
||||
end
|
||||
end
|
||||
table.sort(non_array_part)
|
||||
for i = 1, #non_array_part do
|
||||
local name = non_array_part[i]
|
||||
ret[#ret + 1] = {name, normalize_template(template[name])}
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function templatepart_serialize(part, argaccum, x, len)
|
||||
local extras = {}
|
||||
local extracount = 0
|
||||
for k, v in pairs(x) do
|
||||
extras[k] = v
|
||||
extracount = extracount + 1
|
||||
end
|
||||
for i = 1, #part do
|
||||
extracount = extracount - 1
|
||||
if type(part[i]) == "table" then
|
||||
extras[part[i][1]] = nil
|
||||
len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len)
|
||||
else
|
||||
extras[part[i]] = nil
|
||||
len = len + 1
|
||||
argaccum[len] = x[part[i]]
|
||||
end
|
||||
end
|
||||
if extracount > 0 then
|
||||
argaccum[len + 1] = extras
|
||||
else
|
||||
argaccum[len + 1] = nil
|
||||
end
|
||||
return len + 1
|
||||
end
|
||||
|
||||
local function templatepart_deserialize(ret, part, values, vindex)
|
||||
for i = 1, #part do
|
||||
local name = part[i]
|
||||
if type(name) == "table" then
|
||||
local newret = {}
|
||||
ret[name[1]] = newret
|
||||
vindex = templatepart_deserialize(newret, name[2], values, vindex)
|
||||
else
|
||||
ret[name] = values[vindex]
|
||||
vindex = vindex + 1
|
||||
end
|
||||
end
|
||||
local extras = values[vindex]
|
||||
if extras then
|
||||
for k, v in pairs(extras) do
|
||||
ret[k] = v
|
||||
end
|
||||
end
|
||||
return vindex + 1
|
||||
end
|
||||
|
||||
local function template_serializer_and_deserializer(metatable, template)
|
||||
return function(x)
|
||||
argaccum = {}
|
||||
local len = templatepart_serialize(template, argaccum, x, 0)
|
||||
return unpack(argaccum, 1, len)
|
||||
end, function(...)
|
||||
local ret = {}
|
||||
local len = select("#", ...)
|
||||
local args = {...}
|
||||
templatepart_deserialize(ret, template, args, 1)
|
||||
return setmetatable(ret, metatable)
|
||||
end
|
||||
end
|
||||
|
||||
local function register(metatable, name, serialize, deserialize)
|
||||
name = name or metatable.name
|
||||
serialize = serialize or metatable._serialize
|
||||
deserialize = deserialize or metatable._deserialize
|
||||
if not serialize then
|
||||
if metatable._template then
|
||||
local t = normalize_template(metatable._template)
|
||||
serialize, deserialize = template_serializer_and_deserializer(metatable, t)
|
||||
elseif not deserialize then
|
||||
serialize = default_serialize
|
||||
deserialize = default_deserialize(metatable)
|
||||
else
|
||||
serialize = metatable
|
||||
end
|
||||
end
|
||||
type_check(metatable, "table", "metatable")
|
||||
type_check(name, "string", "name")
|
||||
type_check(serialize, "function", "serialize")
|
||||
type_check(deserialize, "function", "deserialize")
|
||||
assert(not ids[metatable], "Metatable already registered.")
|
||||
assert(not mts[name], ("Name %q already registered."):format(name))
|
||||
mts[name] = metatable
|
||||
ids[metatable] = name
|
||||
serializers[name] = serialize
|
||||
deserializers[name] = deserialize
|
||||
return metatable
|
||||
end
|
||||
|
||||
local function unregister(item)
|
||||
local name, metatable
|
||||
if type(item) == "string" then -- assume name
|
||||
name, metatable = item, mts[item]
|
||||
else -- assume metatable
|
||||
name, metatable = ids[item], item
|
||||
end
|
||||
type_check(name, "string", "name")
|
||||
type_check(metatable, "table", "metatable")
|
||||
mts[name] = nil
|
||||
ids[metatable] = nil
|
||||
serializers[name] = nil
|
||||
deserializers[name] = nil
|
||||
return metatable
|
||||
end
|
||||
|
||||
local function registerClass(class, name)
|
||||
name = name or class.name
|
||||
if class.__instanceDict then -- middleclass
|
||||
register(class.__instanceDict, name)
|
||||
else -- assume 30log or similar library
|
||||
register(class, name)
|
||||
end
|
||||
return class
|
||||
end
|
||||
|
||||
local function registerResource(resource, name)
|
||||
type_check(name, "string", "name")
|
||||
assert(not resources[resource],
|
||||
"Resource already registered.")
|
||||
assert(not resources_by_name[name],
|
||||
format("Resource %q already exists.", name))
|
||||
resources_by_name[name] = resource
|
||||
resources[resource] = name
|
||||
return resource
|
||||
end
|
||||
|
||||
local function unregisterResource(name)
|
||||
type_check(name, "string", "name")
|
||||
assert(resources_by_name[name], format("Resource %q does not exist.", name))
|
||||
local resource = resources_by_name[name]
|
||||
resources_by_name[name] = nil
|
||||
resources[resource] = nil
|
||||
return resource
|
||||
end
|
||||
|
||||
return {
|
||||
-- aliases
|
||||
s = serialize,
|
||||
d = deserialize,
|
||||
dn = deserializeN,
|
||||
r = readFile,
|
||||
w = writeFile,
|
||||
a = appendFile,
|
||||
|
||||
serialize = serialize,
|
||||
deserialize = deserialize,
|
||||
deserializeN = deserializeN,
|
||||
readFile = readFile,
|
||||
writeFile = writeFile,
|
||||
appendFile = appendFile,
|
||||
register = register,
|
||||
unregister = unregister,
|
||||
registerResource = registerResource,
|
||||
unregisterResource = unregisterResource,
|
||||
registerClass = registerClass
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
--
|
||||
-- classic
|
||||
--
|
||||
-- Copyright (c) 2014, rxi
|
||||
--
|
||||
-- This module is free software; you can redistribute it and/or modify it under
|
||||
-- the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
|
||||
local Object = {}
|
||||
Object.__index = Object
|
||||
|
||||
|
||||
function Object:new()
|
||||
end
|
||||
|
||||
|
||||
function Object:extend()
|
||||
local cls = {}
|
||||
for k, v in pairs(self) do
|
||||
if k:find("__") == 1 then
|
||||
cls[k] = v
|
||||
end
|
||||
end
|
||||
cls.__index = cls
|
||||
cls.super = self
|
||||
setmetatable(cls, self)
|
||||
return cls
|
||||
end
|
||||
|
||||
|
||||
function Object:implement(...)
|
||||
for _, cls in pairs({...}) do
|
||||
for k, v in pairs(cls) do
|
||||
if self[k] == nil and type(v) == "function" then
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Object:is(T)
|
||||
local mt = getmetatable(self)
|
||||
while mt do
|
||||
if mt == T then
|
||||
return true
|
||||
end
|
||||
mt = getmetatable(mt)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function Object:__tostring()
|
||||
return "Object"
|
||||
end
|
||||
|
||||
|
||||
function Object:__call(...)
|
||||
local obj = setmetatable({}, self)
|
||||
obj:new(...)
|
||||
return obj
|
||||
end
|
||||
|
||||
|
||||
return Object
|
|
@ -1,99 +0,0 @@
|
|||
--[[
|
||||
CScreen v1.3 by CodeNMore
|
||||
A simple way to make resolution-independent Love2D games
|
||||
Tested for LOVE 0.10.1
|
||||
See: https://github.com/CodeNMore/CScreen
|
||||
Zlib License:
|
||||
Copyright (c) 2016 CodeNMore
|
||||
This software is provided 'as-is', without any express or implied warranty.
|
||||
In no event will the authors be held liable for any damages arising from
|
||||
the use of this software.
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software in
|
||||
a product, an acknowledgment in the product documentation would be appreciated
|
||||
but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
--]]
|
||||
|
||||
local CScreen = {}
|
||||
local rx, ry, ctr = 800, 600, true
|
||||
local rxv, ryv, fsv, fsvr = 800, 600, 1.0, 1.0
|
||||
local tx, ty, rwf, rhf = 0, 0, 800, 600
|
||||
local cr, cg, cb, ca = 0, 0, 0, 255
|
||||
|
||||
-- Initializes CScreen with the initial size values
|
||||
function CScreen.init(tw, th, cntr)
|
||||
rx = tw or 800
|
||||
ry = th or 600
|
||||
ctr = cntr or false
|
||||
CScreen.update(love.graphics.getWidth(), love.graphics.getHeight())
|
||||
end
|
||||
|
||||
-- Draws letterbox borders
|
||||
function CScreen.cease()
|
||||
if ctr then
|
||||
local pr, pg, pb, pa = love.graphics.getColor()
|
||||
love.graphics.setColor(cr, cg, cb, ca)
|
||||
love.graphics.scale(fsvr, fsvr)
|
||||
|
||||
if tx ~= 0 then
|
||||
love.graphics.rectangle("fill", -tx, 0, tx, rhf)
|
||||
love.graphics.rectangle("fill", rxv, 0, tx, rhf)
|
||||
elseif ty ~= 0 then
|
||||
love.graphics.rectangle("fill", 0, -ty, rwf, ty)
|
||||
love.graphics.rectangle("fill", 0, ryv, rwf, ty)
|
||||
end
|
||||
|
||||
love.graphics.setColor(pr, pg, pb, pa)
|
||||
end
|
||||
end
|
||||
|
||||
-- Scales and centers all graphics properly
|
||||
function CScreen.apply()
|
||||
if ctr then
|
||||
love.graphics.translate(tx, ty)
|
||||
end
|
||||
love.graphics.scale(fsv, fsv)
|
||||
end
|
||||
|
||||
-- Updates CScreen when the window size changes
|
||||
function CScreen.update(w, h)
|
||||
local sx = w / rx
|
||||
local sy = h / ry
|
||||
fsv = math.min(sx, sy)
|
||||
fsvr = 1 / fsv
|
||||
-- Centering
|
||||
if ctr and fsv == sx then -- Vertically
|
||||
tx = 0
|
||||
ty = (h / 2) - (ry * fsv / 2)
|
||||
elseif ctr and fsv == sy then -- Horizontally
|
||||
ty = 0
|
||||
tx = (w / 2) - (rx * fsv / 2)
|
||||
end
|
||||
-- Variable sets
|
||||
rwf = w
|
||||
rhf = h
|
||||
rxv = rx * fsv
|
||||
ryv = ry * fsv
|
||||
end
|
||||
|
||||
-- Convert from window coordinates to target coordinates
|
||||
function CScreen.project(x, y)
|
||||
return math.floor((x - tx) / fsv), math.floor((y - ty) / fsv)
|
||||
end
|
||||
|
||||
-- Change letterbox color
|
||||
function CScreen.setColor(r, g, b, a)
|
||||
cr = r
|
||||
cg = g
|
||||
cb = b
|
||||
ca = a
|
||||
end
|
||||
|
||||
-- Return the table for use
|
||||
return CScreen
|
|
@ -1,216 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2010-2015 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or ''
|
||||
local cos, sin = math.cos, math.sin
|
||||
|
||||
local camera = {}
|
||||
camera.__index = camera
|
||||
|
||||
-- Movement interpolators (for camera locking/windowing)
|
||||
camera.smooth = {}
|
||||
|
||||
function camera.smooth.none()
|
||||
return function(dx,dy) return dx,dy end
|
||||
end
|
||||
|
||||
function camera.smooth.linear(speed)
|
||||
assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed))
|
||||
return function(dx,dy, s)
|
||||
-- normalize direction
|
||||
local d = math.sqrt(dx*dx+dy*dy)
|
||||
local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal
|
||||
if d > 0 then
|
||||
dx,dy = dx/d, dy/d
|
||||
end
|
||||
|
||||
return dx*dts, dy*dts
|
||||
end
|
||||
end
|
||||
|
||||
function camera.smooth.damped(stiffness)
|
||||
assert(type(stiffness) == "number", "Invalid parameter: stiffness = "..tostring(stiffness))
|
||||
return function(dx,dy, s)
|
||||
local dts = love.timer.getDelta() * (s or stiffness)
|
||||
return dx*dts, dy*dts
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function new(x,y, zoom, rot, smoother)
|
||||
x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2
|
||||
zoom = zoom or 1
|
||||
rot = rot or 0
|
||||
smoother = smoother or camera.smooth.none() -- for locking, see below
|
||||
return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera)
|
||||
end
|
||||
|
||||
function camera:lookAt(x,y)
|
||||
self.x, self.y = x, y
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:move(dx,dy)
|
||||
self.x, self.y = self.x + dx, self.y + dy
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:position()
|
||||
return self.x, self.y
|
||||
end
|
||||
|
||||
function camera:rotate(phi)
|
||||
self.rot = self.rot + phi
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:rotateTo(phi)
|
||||
self.rot = phi
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:zoom(mul)
|
||||
self.scale = self.scale * mul
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:zoomTo(zoom)
|
||||
self.scale = zoom
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:attach(x,y,w,h, noclip)
|
||||
x,y = x or 0, y or 0
|
||||
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||
|
||||
self._sx,self._sy,self._sw,self._sh = love.graphics.getScissor()
|
||||
if not noclip then
|
||||
love.graphics.setScissor(x,y,w,h)
|
||||
end
|
||||
|
||||
local cx,cy = x+w/2, y+h/2
|
||||
love.graphics.push()
|
||||
love.graphics.translate(cx, cy)
|
||||
love.graphics.scale(self.scale)
|
||||
love.graphics.rotate(self.rot)
|
||||
love.graphics.translate(-self.x, -self.y)
|
||||
end
|
||||
|
||||
function camera:detach()
|
||||
love.graphics.pop()
|
||||
love.graphics.setScissor(self._sx,self._sy,self._sw,self._sh)
|
||||
end
|
||||
|
||||
function camera:draw(...)
|
||||
local x,y,w,h,noclip,func
|
||||
local nargs = select("#", ...)
|
||||
if nargs == 1 then
|
||||
func = ...
|
||||
elseif nargs == 5 then
|
||||
x,y,w,h,func = ...
|
||||
elseif nargs == 6 then
|
||||
x,y,w,h,noclip,func = ...
|
||||
else
|
||||
error("Invalid arguments to camera:draw()")
|
||||
end
|
||||
|
||||
self:attach(x,y,w,h,noclip)
|
||||
func()
|
||||
self:detach()
|
||||
end
|
||||
|
||||
-- world coordinates to camera coordinates
|
||||
function camera:cameraCoords(x,y, ox,oy,w,h)
|
||||
ox, oy = ox or 0, oy or 0
|
||||
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||
|
||||
-- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center
|
||||
local c,s = cos(self.rot), sin(self.rot)
|
||||
x,y = x - self.x, y - self.y
|
||||
x,y = c*x - s*y, s*x + c*y
|
||||
return x*self.scale + w/2 + ox, y*self.scale + h/2 + oy
|
||||
end
|
||||
|
||||
-- camera coordinates to world coordinates
|
||||
function camera:worldCoords(x,y, ox,oy,w,h)
|
||||
ox, oy = ox or 0, oy or 0
|
||||
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
|
||||
|
||||
-- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y)
|
||||
local c,s = cos(-self.rot), sin(-self.rot)
|
||||
x,y = (x - w/2 - ox) / self.scale, (y - h/2 - oy) / self.scale
|
||||
x,y = c*x - s*y, s*x + c*y
|
||||
return x+self.x, y+self.y
|
||||
end
|
||||
|
||||
function camera:mousePosition(ox,oy,w,h)
|
||||
local mx,my = love.mouse.getPosition()
|
||||
return self:worldCoords(mx,my, ox,oy,w,h)
|
||||
end
|
||||
|
||||
-- camera scrolling utilities
|
||||
function camera:lockX(x, smoother, ...)
|
||||
local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...)
|
||||
self.x = self.x + dx
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:lockY(y, smoother, ...)
|
||||
local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...)
|
||||
self.y = self.y + dy
|
||||
return self
|
||||
end
|
||||
|
||||
function camera:lockPosition(x,y, smoother, ...)
|
||||
return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...))
|
||||
end
|
||||
|
||||
function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...)
|
||||
-- figure out displacement in camera coordinates
|
||||
x,y = self:cameraCoords(x,y)
|
||||
local dx, dy = 0,0
|
||||
if x < x_min then
|
||||
dx = x - x_min
|
||||
elseif x > x_max then
|
||||
dx = x - x_max
|
||||
end
|
||||
if y < y_min then
|
||||
dy = y - y_min
|
||||
elseif y > y_max then
|
||||
dy = y - y_max
|
||||
end
|
||||
|
||||
-- transform displacement to movement in world coordinates
|
||||
local c,s = cos(-self.rot), sin(-self.rot)
|
||||
dx,dy = (c*dx - s*dy) / self.scale, (s*dx + c*dy) / self.scale
|
||||
|
||||
-- move
|
||||
self:move((smoother or self.smoother)(dx,dy,...))
|
||||
end
|
||||
|
||||
-- the module
|
||||
return setmetatable({new = new, smooth = camera.smooth},
|
||||
{__call = function(_, ...) return new(...) end})
|
|
@ -1,98 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2010-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local function include_helper(to, from, seen)
|
||||
if from == nil then
|
||||
return to
|
||||
elseif type(from) ~= 'table' then
|
||||
return from
|
||||
elseif seen[from] then
|
||||
return seen[from]
|
||||
end
|
||||
|
||||
seen[from] = to
|
||||
for k,v in pairs(from) do
|
||||
k = include_helper({}, k, seen) -- keys might also be tables
|
||||
if to[k] == nil then
|
||||
to[k] = include_helper({}, v, seen)
|
||||
end
|
||||
end
|
||||
return to
|
||||
end
|
||||
|
||||
-- deeply copies `other' into `class'. keys in `other' that are already
|
||||
-- defined in `class' are omitted
|
||||
local function include(class, other)
|
||||
return include_helper(class, other, {})
|
||||
end
|
||||
|
||||
-- returns a deep copy of `other'
|
||||
local function clone(other)
|
||||
return setmetatable(include({}, other), getmetatable(other))
|
||||
end
|
||||
|
||||
local function new(class)
|
||||
-- mixins
|
||||
class = class or {} -- class can be nil
|
||||
local inc = class.__includes or {}
|
||||
if getmetatable(inc) then inc = {inc} end
|
||||
|
||||
for _, other in ipairs(inc) do
|
||||
if type(other) == "string" then
|
||||
other = _G[other]
|
||||
end
|
||||
include(class, other)
|
||||
end
|
||||
|
||||
-- class implementation
|
||||
class.__index = class
|
||||
class.init = class.init or class[1] or function() end
|
||||
class.include = class.include or include
|
||||
class.clone = class.clone or clone
|
||||
|
||||
-- constructor call
|
||||
return setmetatable(class, {__call = function(c, ...)
|
||||
local o = setmetatable({}, c)
|
||||
o:init(...)
|
||||
return o
|
||||
end})
|
||||
end
|
||||
|
||||
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
|
||||
if class_commons ~= false and not common then
|
||||
common = {}
|
||||
function common.class(name, prototype, parent)
|
||||
return new{__includes = {prototype, parent}}
|
||||
end
|
||||
function common.instance(class, ...)
|
||||
return class(...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- the module
|
||||
return setmetatable({new = new, include = include, clone = clone},
|
||||
{__call = function(_,...) return new(...) end})
|
|
@ -1,108 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2010-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local function __NULL__() end
|
||||
|
||||
-- default gamestate produces error on every callback
|
||||
local state_init = setmetatable({leave = __NULL__},
|
||||
{__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end})
|
||||
local stack = {state_init}
|
||||
local initialized_states = setmetatable({}, {__mode = "k"})
|
||||
local state_is_dirty = true
|
||||
|
||||
local GS = {}
|
||||
function GS.new(t) return t or {} end -- constructor - deprecated!
|
||||
|
||||
local function change_state(stack_offset, to, ...)
|
||||
local pre = stack[#stack]
|
||||
|
||||
-- initialize only on first call
|
||||
;(initialized_states[to] or to.init or __NULL__)(to)
|
||||
initialized_states[to] = __NULL__
|
||||
|
||||
stack[#stack+stack_offset] = to
|
||||
state_is_dirty = true
|
||||
return (to.enter or __NULL__)(to, pre, ...)
|
||||
end
|
||||
|
||||
function GS.switch(to, ...)
|
||||
assert(to, "Missing argument: Gamestate to switch to")
|
||||
assert(to ~= GS, "Can't call switch with colon operator")
|
||||
;(stack[#stack].leave or __NULL__)(stack[#stack])
|
||||
return change_state(0, to, ...)
|
||||
end
|
||||
|
||||
function GS.push(to, ...)
|
||||
assert(to, "Missing argument: Gamestate to switch to")
|
||||
assert(to ~= GS, "Can't call push with colon operator")
|
||||
return change_state(1, to, ...)
|
||||
end
|
||||
|
||||
function GS.pop(...)
|
||||
assert(#stack > 1, "No more states to pop!")
|
||||
local pre, to = stack[#stack], stack[#stack-1]
|
||||
stack[#stack] = nil
|
||||
;(pre.leave or __NULL__)(pre)
|
||||
state_is_dirty = true
|
||||
return (to.resume or __NULL__)(to, pre, ...)
|
||||
end
|
||||
|
||||
function GS.current()
|
||||
return stack[#stack]
|
||||
end
|
||||
|
||||
-- fetch event callbacks from love.handlers
|
||||
local all_callbacks = { 'draw', 'errhand', 'update' }
|
||||
for k in pairs(love.handlers) do
|
||||
all_callbacks[#all_callbacks+1] = k
|
||||
end
|
||||
|
||||
function GS.registerEvents(callbacks)
|
||||
local registry = {}
|
||||
callbacks = callbacks or all_callbacks
|
||||
for _, f in ipairs(callbacks) do
|
||||
registry[f] = love[f] or __NULL__
|
||||
love[f] = function(...)
|
||||
registry[f](...)
|
||||
return GS[f](...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- forward any undefined functions
|
||||
setmetatable(GS, {__index = function(_, func)
|
||||
-- call function only if at least one 'update' was called beforehand
|
||||
-- (see issue #46)
|
||||
if not state_is_dirty or func == 'update' then
|
||||
state_is_dirty = false
|
||||
return function(...)
|
||||
return (stack[#stack][func] or __NULL__)(stack[#stack], ...)
|
||||
end
|
||||
end
|
||||
return __NULL__
|
||||
end})
|
||||
|
||||
return GS
|
|
@ -1,102 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2012-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local Registry = {}
|
||||
Registry.__index = function(self, key)
|
||||
return Registry[key] or (function()
|
||||
local t = {}
|
||||
rawset(self, key, t)
|
||||
return t
|
||||
end)()
|
||||
end
|
||||
|
||||
function Registry:register(s, f)
|
||||
self[s][f] = f
|
||||
return f
|
||||
end
|
||||
|
||||
function Registry:emit(s, ...)
|
||||
for f in pairs(self[s]) do
|
||||
f(...)
|
||||
end
|
||||
end
|
||||
|
||||
function Registry:remove(s, ...)
|
||||
local f = {...}
|
||||
for i = 1,select('#', ...) do
|
||||
self[s][f[i]] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Registry:clear(...)
|
||||
local s = {...}
|
||||
for i = 1,select('#', ...) do
|
||||
self[s[i]] = {}
|
||||
end
|
||||
end
|
||||
|
||||
function Registry:emitPattern(p, ...)
|
||||
for s in pairs(self) do
|
||||
if s:match(p) then self:emit(s, ...) end
|
||||
end
|
||||
end
|
||||
|
||||
function Registry:registerPattern(p, f)
|
||||
for s in pairs(self) do
|
||||
if s:match(p) then self:register(s, f) end
|
||||
end
|
||||
return f
|
||||
end
|
||||
|
||||
function Registry:removePattern(p, ...)
|
||||
for s in pairs(self) do
|
||||
if s:match(p) then self:remove(s, ...) end
|
||||
end
|
||||
end
|
||||
|
||||
function Registry:clearPattern(p)
|
||||
for s in pairs(self) do
|
||||
if s:match(p) then self[s] = {} end
|
||||
end
|
||||
end
|
||||
|
||||
-- instancing
|
||||
function Registry.new()
|
||||
return setmetatable({}, Registry)
|
||||
end
|
||||
|
||||
-- default instance
|
||||
local default = Registry.new()
|
||||
|
||||
-- module forwards calls to default instance
|
||||
local module = {}
|
||||
for k in pairs(Registry) do
|
||||
if k ~= "__index" then
|
||||
module[k] = function(...) return default[k](default, ...) end
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(module, {__call = Registry.new})
|
|
@ -1,210 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2010-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local Timer = {}
|
||||
Timer.__index = Timer
|
||||
|
||||
local function _nothing_() end
|
||||
|
||||
function Timer:update(dt)
|
||||
local to_remove = {}
|
||||
|
||||
for handle in pairs(self.functions) do
|
||||
-- handle: {
|
||||
-- time = <number>,
|
||||
-- after = <function>,
|
||||
-- during = <function>,
|
||||
-- limit = <number>,
|
||||
-- count = <number>,
|
||||
-- }
|
||||
|
||||
handle.time = handle.time + dt
|
||||
handle.during(dt, math.max(handle.limit - handle.time, 0))
|
||||
|
||||
while handle.time >= handle.limit and handle.count > 0 do
|
||||
if handle.after(handle.after) == false then
|
||||
handle.count = 0
|
||||
break
|
||||
end
|
||||
handle.time = handle.time - handle.limit
|
||||
handle.count = handle.count - 1
|
||||
end
|
||||
|
||||
if handle.count == 0 then
|
||||
table.insert(to_remove, handle)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #to_remove do
|
||||
self.functions[to_remove[i]] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Timer:during(delay, during, after)
|
||||
local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
|
||||
self.functions[handle] = true
|
||||
return handle
|
||||
end
|
||||
|
||||
function Timer:after(delay, func)
|
||||
return self:during(delay, _nothing_, func)
|
||||
end
|
||||
|
||||
function Timer:every(delay, after, count)
|
||||
local count = count or math.huge -- exploit below: math.huge - 1 = math.huge
|
||||
local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
|
||||
self.functions[handle] = true
|
||||
return handle
|
||||
end
|
||||
|
||||
function Timer:cancel(handle)
|
||||
self.functions[handle] = nil
|
||||
end
|
||||
|
||||
function Timer:clear()
|
||||
self.functions = {}
|
||||
end
|
||||
|
||||
function Timer:script(f)
|
||||
local co = coroutine.wrap(f)
|
||||
co(function(t)
|
||||
self:after(t, co)
|
||||
coroutine.yield()
|
||||
end)
|
||||
end
|
||||
|
||||
Timer.tween = setmetatable({
|
||||
-- helper functions
|
||||
out = function(f) -- 'rotates' a function
|
||||
return function(s, ...) return 1 - f(1-s, ...) end
|
||||
end,
|
||||
chain = function(f1, f2) -- concatenates two functions
|
||||
return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end
|
||||
end,
|
||||
|
||||
-- useful tweening functions
|
||||
linear = function(s) return s end,
|
||||
quad = function(s) return s*s end,
|
||||
cubic = function(s) return s*s*s end,
|
||||
quart = function(s) return s*s*s*s end,
|
||||
quint = function(s) return s*s*s*s*s end,
|
||||
sine = function(s) return 1-math.cos(s*math.pi/2) end,
|
||||
expo = function(s) return 2^(10*(s-1)) end,
|
||||
circ = function(s) return 1 - math.sqrt(1-s*s) end,
|
||||
|
||||
back = function(s,bounciness)
|
||||
bounciness = bounciness or 1.70158
|
||||
return s*s*((bounciness+1)*s - bounciness)
|
||||
end,
|
||||
|
||||
bounce = function(s) -- magic numbers ahead
|
||||
local a,b = 7.5625, 1/2.75
|
||||
return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375)
|
||||
end,
|
||||
|
||||
elastic = function(s, amp, period)
|
||||
amp, period = amp and math.max(1, amp) or 1, period or .3
|
||||
return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1))
|
||||
end,
|
||||
}, {
|
||||
|
||||
-- register new tween
|
||||
__call = function(tween, self, len, subject, target, method, after, ...)
|
||||
-- recursively collects fields that are defined in both subject and target into a flat list
|
||||
local function tween_collect_payload(subject, target, out)
|
||||
for k,v in pairs(target) do
|
||||
local ref = subject[k]
|
||||
assert(type(v) == type(ref), 'Type mismatch in field "'..k..'".')
|
||||
if type(v) == 'table' then
|
||||
tween_collect_payload(ref, v, out)
|
||||
else
|
||||
local ok, delta = pcall(function() return (v-ref)*1 end)
|
||||
assert(ok, 'Field "'..k..'" does not support arithmetic operations')
|
||||
out[#out+1] = {subject, k, delta}
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
method = tween[method or 'linear'] -- see __index
|
||||
local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...}
|
||||
|
||||
local last_s = 0
|
||||
return self:during(len, function(dt)
|
||||
t = t + dt
|
||||
local s = method(math.min(1, t/len), unpack(args))
|
||||
local ds = s - last_s
|
||||
last_s = s
|
||||
for _, info in ipairs(payload) do
|
||||
local ref, key, delta = unpack(info)
|
||||
ref[key] = ref[key] + delta * ds
|
||||
end
|
||||
end, after)
|
||||
end,
|
||||
|
||||
-- fetches function and generated compositions for method `key`
|
||||
__index = function(tweens, key)
|
||||
if type(key) == 'function' then return key end
|
||||
|
||||
assert(type(key) == 'string', 'Method must be function or string.')
|
||||
if rawget(tweens, key) then return rawget(tweens, key) end
|
||||
|
||||
local function construct(pattern, f)
|
||||
local method = rawget(tweens, key:match(pattern))
|
||||
if method then return f(method) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local out, chain = rawget(tweens,'out'), rawget(tweens,'chain')
|
||||
return construct('^in%-([^-]+)$', function(...) return ... end)
|
||||
or construct('^out%-([^-]+)$', out)
|
||||
or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
|
||||
or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
|
||||
or error('Unknown interpolation method: ' .. key)
|
||||
end})
|
||||
|
||||
-- Timer instancing
|
||||
function Timer.new()
|
||||
return setmetatable({functions = {}, tween = Timer.tween}, Timer)
|
||||
end
|
||||
|
||||
-- default instance
|
||||
local default = Timer.new()
|
||||
|
||||
-- module forwards calls to default instance
|
||||
local module = {}
|
||||
for k in pairs(Timer) do
|
||||
if k ~= "__index" then
|
||||
module[k] = function(...) return default[k](default, ...) end
|
||||
end
|
||||
end
|
||||
module.tween = setmetatable({}, {
|
||||
__index = Timer.tween,
|
||||
__newindex = function(k,v) Timer.tween[k] = v end,
|
||||
__call = function(t, ...) return default:tween(...) end,
|
||||
})
|
||||
|
||||
return setmetatable(module, {__call = Timer.new})
|
|
@ -1,172 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2012-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2
|
||||
|
||||
local function str(x,y)
|
||||
return "("..tonumber(x)..","..tonumber(y)..")"
|
||||
end
|
||||
|
||||
local function mul(s, x,y)
|
||||
return s*x, s*y
|
||||
end
|
||||
|
||||
local function div(s, x,y)
|
||||
return x/s, y/s
|
||||
end
|
||||
|
||||
local function add(x1,y1, x2,y2)
|
||||
return x1+x2, y1+y2
|
||||
end
|
||||
|
||||
local function sub(x1,y1, x2,y2)
|
||||
return x1-x2, y1-y2
|
||||
end
|
||||
|
||||
local function permul(x1,y1, x2,y2)
|
||||
return x1*x2, y1*y2
|
||||
end
|
||||
|
||||
local function dot(x1,y1, x2,y2)
|
||||
return x1*x2 + y1*y2
|
||||
end
|
||||
|
||||
local function det(x1,y1, x2,y2)
|
||||
return x1*y2 - y1*x2
|
||||
end
|
||||
|
||||
local function eq(x1,y1, x2,y2)
|
||||
return x1 == x2 and y1 == y2
|
||||
end
|
||||
|
||||
local function lt(x1,y1, x2,y2)
|
||||
return x1 < x2 or (x1 == x2 and y1 < y2)
|
||||
end
|
||||
|
||||
local function le(x1,y1, x2,y2)
|
||||
return x1 <= x2 and y1 <= y2
|
||||
end
|
||||
|
||||
local function len2(x,y)
|
||||
return x*x + y*y
|
||||
end
|
||||
|
||||
local function len(x,y)
|
||||
return sqrt(x*x + y*y)
|
||||
end
|
||||
|
||||
local function fromPolar(angle, radius)
|
||||
return cos(angle)*radius, sin(angle)*radius
|
||||
end
|
||||
|
||||
local function toPolar(x, y)
|
||||
return atan2(y,x), len(x,y)
|
||||
end
|
||||
|
||||
local function dist2(x1,y1, x2,y2)
|
||||
return len2(x1-x2, y1-y2)
|
||||
end
|
||||
|
||||
local function dist(x1,y1, x2,y2)
|
||||
return len(x1-x2, y1-y2)
|
||||
end
|
||||
|
||||
local function normalize(x,y)
|
||||
local l = len(x,y)
|
||||
if l > 0 then
|
||||
return x/l, y/l
|
||||
end
|
||||
return x,y
|
||||
end
|
||||
|
||||
local function rotate(phi, x,y)
|
||||
local c, s = cos(phi), sin(phi)
|
||||
return c*x - s*y, s*x + c*y
|
||||
end
|
||||
|
||||
local function perpendicular(x,y)
|
||||
return -y, x
|
||||
end
|
||||
|
||||
local function project(x,y, u,v)
|
||||
local s = (x*u + y*v) / (u*u + v*v)
|
||||
return s*u, s*v
|
||||
end
|
||||
|
||||
local function mirror(x,y, u,v)
|
||||
local s = 2 * (x*u + y*v) / (u*u + v*v)
|
||||
return s*u - x, s*v - y
|
||||
end
|
||||
|
||||
-- ref.: http://blog.signalsondisplay.com/?p=336
|
||||
local function trim(maxLen, x, y)
|
||||
local s = maxLen * maxLen / len2(x, y)
|
||||
s = s > 1 and 1 or math.sqrt(s)
|
||||
return x * s, y * s
|
||||
end
|
||||
|
||||
local function angleTo(x,y, u,v)
|
||||
if u and v then
|
||||
return atan2(y, x) - atan2(v, u)
|
||||
end
|
||||
return atan2(y, x)
|
||||
end
|
||||
|
||||
-- the module
|
||||
return {
|
||||
str = str,
|
||||
|
||||
fromPolar = fromPolar,
|
||||
toPolar = toPolar,
|
||||
|
||||
-- arithmetic
|
||||
mul = mul,
|
||||
div = div,
|
||||
add = add,
|
||||
sub = sub,
|
||||
permul = permul,
|
||||
dot = dot,
|
||||
det = det,
|
||||
cross = det,
|
||||
|
||||
-- relation
|
||||
eq = eq,
|
||||
lt = lt,
|
||||
le = le,
|
||||
|
||||
-- misc operations
|
||||
len2 = len2,
|
||||
len = len,
|
||||
dist2 = dist2,
|
||||
dist = dist,
|
||||
normalize = normalize,
|
||||
rotate = rotate,
|
||||
perpendicular = perpendicular,
|
||||
project = project,
|
||||
mirror = mirror,
|
||||
trim = trim,
|
||||
angleTo = angleTo,
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
--[[
|
||||
Copyright (c) 2010-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local assert = assert
|
||||
local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2
|
||||
|
||||
local vector = {}
|
||||
vector.__index = vector
|
||||
|
||||
local function new(x,y)
|
||||
return setmetatable({x = x or 0, y = y or 0}, vector)
|
||||
end
|
||||
local zero = new(0,0)
|
||||
|
||||
local function fromPolar(angle, radius)
|
||||
return new(cos(angle) * radius, sin(angle) * radius)
|
||||
end
|
||||
|
||||
local function isvector(v)
|
||||
return type(v) == 'table' and type(v.x) == 'number' and type(v.y) == 'number'
|
||||
end
|
||||
|
||||
function vector:clone()
|
||||
return new(self.x, self.y)
|
||||
end
|
||||
|
||||
function vector:unpack()
|
||||
return self.x, self.y
|
||||
end
|
||||
|
||||
function vector:__tostring()
|
||||
return "("..tonumber(self.x)..","..tonumber(self.y)..")"
|
||||
end
|
||||
|
||||
function vector.__unm(a)
|
||||
return new(-a.x, -a.y)
|
||||
end
|
||||
|
||||
function vector.__add(a,b)
|
||||
assert(isvector(a) and isvector(b), "Add: wrong argument types (<vector> expected)")
|
||||
return new(a.x+b.x, a.y+b.y)
|
||||
end
|
||||
|
||||
function vector.__sub(a,b)
|
||||
assert(isvector(a) and isvector(b), "Sub: wrong argument types (<vector> expected)")
|
||||
return new(a.x-b.x, a.y-b.y)
|
||||
end
|
||||
|
||||
function vector.__mul(a,b)
|
||||
if type(a) == "number" then
|
||||
return new(a*b.x, a*b.y)
|
||||
elseif type(b) == "number" then
|
||||
return new(b*a.x, b*a.y)
|
||||
else
|
||||
assert(isvector(a) and isvector(b), "Mul: wrong argument types (<vector> or <number> expected)")
|
||||
return a.x*b.x + a.y*b.y
|
||||
end
|
||||
end
|
||||
|
||||
function vector.__div(a,b)
|
||||
assert(isvector(a) and type(b) == "number", "wrong argument types (expected <vector> / <number>)")
|
||||
return new(a.x / b, a.y / b)
|
||||
end
|
||||
|
||||
function vector.__eq(a,b)
|
||||
return a.x == b.x and a.y == b.y
|
||||
end
|
||||
|
||||
function vector.__lt(a,b)
|
||||
return a.x < b.x or (a.x == b.x and a.y < b.y)
|
||||
end
|
||||
|
||||
function vector.__le(a,b)
|
||||
return a.x <= b.x and a.y <= b.y
|
||||
end
|
||||
|
||||
function vector.permul(a,b)
|
||||
assert(isvector(a) and isvector(b), "permul: wrong argument types (<vector> expected)")
|
||||
return new(a.x*b.x, a.y*b.y)
|
||||
end
|
||||
|
||||
function vector:toPolar()
|
||||
return new(atan2(self.x, self.y), self:len())
|
||||
end
|
||||
|
||||
function vector:len2()
|
||||
return self.x * self.x + self.y * self.y
|
||||
end
|
||||
|
||||
function vector:len()
|
||||
return sqrt(self.x * self.x + self.y * self.y)
|
||||
end
|
||||
|
||||
function vector.dist(a, b)
|
||||
assert(isvector(a) and isvector(b), "dist: wrong argument types (<vector> expected)")
|
||||
local dx = a.x - b.x
|
||||
local dy = a.y - b.y
|
||||
return sqrt(dx * dx + dy * dy)
|
||||
end
|
||||
|
||||
function vector.dist2(a, b)
|
||||
assert(isvector(a) and isvector(b), "dist: wrong argument types (<vector> expected)")
|
||||
local dx = a.x - b.x
|
||||
local dy = a.y - b.y
|
||||
return (dx * dx + dy * dy)
|
||||
end
|
||||
|
||||
function vector:normalizeInplace()
|
||||
local l = self:len()
|
||||
if l > 0 then
|
||||
self.x, self.y = self.x / l, self.y / l
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function vector:normalized()
|
||||
return self:clone():normalizeInplace()
|
||||
end
|
||||
|
||||
function vector:rotateInplace(phi)
|
||||
local c, s = cos(phi), sin(phi)
|
||||
self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y
|
||||
return self
|
||||
end
|
||||
|
||||
function vector:rotated(phi)
|
||||
local c, s = cos(phi), sin(phi)
|
||||
return new(c * self.x - s * self.y, s * self.x + c * self.y)
|
||||
end
|
||||
|
||||
function vector:perpendicular()
|
||||
return new(-self.y, self.x)
|
||||
end
|
||||
|
||||
function vector:projectOn(v)
|
||||
assert(isvector(v), "invalid argument: cannot project vector on " .. type(v))
|
||||
-- (self * v) * v / v:len2()
|
||||
local s = (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y)
|
||||
return new(s * v.x, s * v.y)
|
||||
end
|
||||
|
||||
function vector:mirrorOn(v)
|
||||
assert(isvector(v), "invalid argument: cannot mirror vector on " .. type(v))
|
||||
-- 2 * self:projectOn(v) - self
|
||||
local s = 2 * (self.x * v.x + self.y * v.y) / (v.x * v.x + v.y * v.y)
|
||||
return new(s * v.x - self.x, s * v.y - self.y)
|
||||
end
|
||||
|
||||
function vector:cross(v)
|
||||
assert(isvector(v), "cross: wrong argument types (<vector> expected)")
|
||||
return self.x * v.y - self.y * v.x
|
||||
end
|
||||
|
||||
-- ref.: http://blog.signalsondisplay.com/?p=336
|
||||
function vector:trimInplace(maxLen)
|
||||
local s = maxLen * maxLen / self:len2()
|
||||
s = (s > 1 and 1) or math.sqrt(s)
|
||||
self.x, self.y = self.x * s, self.y * s
|
||||
return self
|
||||
end
|
||||
|
||||
function vector:angleTo(other)
|
||||
if other then
|
||||
return atan2(self.y, self.x) - atan2(other.y, other.x)
|
||||
end
|
||||
return atan2(self.y, self.x)
|
||||
end
|
||||
|
||||
function vector:trimmed(maxLen)
|
||||
return self:clone():trimInplace(maxLen)
|
||||
end
|
||||
|
||||
|
||||
-- the module
|
||||
return setmetatable({new = new, fromPolar = fromPolar, isvector = isvector, zero = zero},
|
||||
{__call = function(_, ...) return new(...) end})
|
|
@ -1,947 +0,0 @@
|
|||
local bump = {
|
||||
_VERSION = 'bump-3dpd v0.2.0',
|
||||
_URL = 'https://github.com/oniietzschan/bump-3dpd',
|
||||
_DESCRIPTION = 'A 3D collision detection library for Lua.',
|
||||
_LICENSE = [[
|
||||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2014 Enrique García Cota
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
}
|
||||
|
||||
------------------------------------------
|
||||
-- Auxiliary functions
|
||||
------------------------------------------
|
||||
local DELTA = 1e-10 -- floating-point margin of error
|
||||
|
||||
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
|
||||
|
||||
local function sign(x)
|
||||
if x > 0 then return 1 end
|
||||
if x == 0 then return 0 end
|
||||
return -1
|
||||
end
|
||||
|
||||
local function nearest(x, a, b)
|
||||
if abs(a - x) < abs(b - x) then return a else return b end
|
||||
end
|
||||
|
||||
local function assertType(desiredType, value, name)
|
||||
if type(value) ~= desiredType then
|
||||
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
|
||||
end
|
||||
end
|
||||
|
||||
local function assertIsPositiveNumber(value, name)
|
||||
if type(value) ~= 'number' or value <= 0 then
|
||||
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
|
||||
end
|
||||
end
|
||||
|
||||
local function assertIsCube(x,y,z,w,h,d)
|
||||
assertType('number', x, 'x')
|
||||
assertType('number', y, 'y')
|
||||
assertType('number', z, 'z')
|
||||
assertIsPositiveNumber(w, 'w')
|
||||
assertIsPositiveNumber(h, 'h')
|
||||
assertIsPositiveNumber(d, 'd')
|
||||
end
|
||||
|
||||
local defaultFilter = function()
|
||||
return 'slide'
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- Cube functions
|
||||
------------------------------------------
|
||||
|
||||
local function cube_getNearestCorner(x,y,z,w,h,d, px, py, pz)
|
||||
return nearest(px, x, x + w),
|
||||
nearest(py, y, y + h),
|
||||
nearest(pz, z, z + d)
|
||||
end
|
||||
|
||||
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
|
||||
-- the normals of the sides where the segment intersects.
|
||||
-- Returns nil if the segment never touches the cube
|
||||
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
|
||||
local function cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1,x2,y2,z2, ti1,ti2)
|
||||
ti1, ti2 = ti1 or 0, ti2 or 1
|
||||
local dx = x2 - x1
|
||||
local dy = y2 - y1
|
||||
local dz = z2 - z1
|
||||
local nx, ny, nz
|
||||
local nx1, ny1, nz1, nx2, ny2, nz2 = 0,0,0,0,0,0
|
||||
local p, q, r
|
||||
|
||||
for side = 1,6 do
|
||||
if side == 1 then -- Left
|
||||
nx,ny,nz,p,q = -1, 0, 0, -dx, x1 - x
|
||||
elseif side == 2 then -- Right
|
||||
nx,ny,nz,p,q = 1, 0, 0, dx, x + w - x1
|
||||
elseif side == 3 then -- Top
|
||||
nx,ny,nz,p,q = 0, -1, 0, -dy, y1 - y
|
||||
elseif side == 4 then -- Bottom
|
||||
nx,ny,nz,p,q = 0, 1, 0, dy, y + h - y1
|
||||
elseif side == 5 then -- Front
|
||||
nx,ny,nz,p,q = 0, 0, -1, -dz, z1 - z
|
||||
else -- Back
|
||||
nx,ny,nz,p,q = 0, 0, 1, dz, z + d - z1
|
||||
end
|
||||
|
||||
if p == 0 then
|
||||
if q <= 0 then
|
||||
return nil
|
||||
end
|
||||
else
|
||||
r = q / p
|
||||
if p < 0 then
|
||||
if r > ti2 then
|
||||
return nil
|
||||
elseif r > ti1 then
|
||||
ti1, nx1,ny1,nz1 = r, nx,ny,nz
|
||||
end
|
||||
else -- p > 0
|
||||
if r < ti1 then
|
||||
return nil
|
||||
elseif r < ti2 then
|
||||
ti2, nx2,ny2,nz2 = r,nx,ny,nz
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ti1,ti2, nx1,ny1,nz1, nx2,ny2,nz2
|
||||
end
|
||||
|
||||
-- Calculates the minkowsky difference between 2 cubes, which is another cube
|
||||
local function cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||
return x2 - x1 - w1,
|
||||
y2 - y1 - h1,
|
||||
z2 - z1 - d1,
|
||||
w1 + w2,
|
||||
h1 + h2,
|
||||
d1 + d2
|
||||
end
|
||||
|
||||
local function cube_containsPoint(x,y,z,w,h,d, px,py,pz)
|
||||
return px - x > DELTA
|
||||
and py - y > DELTA
|
||||
and pz - z > DELTA
|
||||
and x + w - px > DELTA
|
||||
and y + h - py > DELTA
|
||||
and z + d - pz > DELTA
|
||||
end
|
||||
|
||||
local function cube_isIntersecting(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||
return x1 < x2 + w2 and x2 < x1 + w1 and
|
||||
y1 < y2 + h2 and y2 < y1 + h1 and
|
||||
z1 < z2 + d2 and z2 < z1 + d1
|
||||
end
|
||||
|
||||
local function cube_getCubeDistance(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||
local dx = x1 - x2 + (w1 - w2)/2
|
||||
local dy = y1 - y2 + (h1 - h2)/2
|
||||
local dz = z1 - z2 + (d1 - d2)/2
|
||||
return (dx * dx) + (dy * dy) + (dz * dz)
|
||||
end
|
||||
|
||||
local function cube_detectCollision(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2, goalX, goalY, goalZ)
|
||||
goalX = goalX or x1
|
||||
goalY = goalY or y1
|
||||
goalZ = goalZ or z1
|
||||
|
||||
local dx = goalX - x1
|
||||
local dy = goalY - y1
|
||||
local dz = goalZ - z1
|
||||
local x,y,z,w,h,d = cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
|
||||
|
||||
local overlaps, ti, nx, ny, nz
|
||||
|
||||
if cube_containsPoint(x,y,z,w,h,d, 0,0,0) then -- item was intersecting other
|
||||
local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
|
||||
-- Volume of intersection:
|
||||
local wi = min(w1, abs(px))
|
||||
local hi = min(h1, abs(py))
|
||||
local di = min(d1, abs(pz))
|
||||
ti = wi * hi * di * -1 -- ti is the negative volume of intersection
|
||||
overlaps = true
|
||||
else
|
||||
local ti1,ti2,nx1,ny1,nz1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, math.huge)
|
||||
|
||||
-- item tunnels into other
|
||||
if ti1
|
||||
and ti1 < 1
|
||||
and (abs(ti1 - ti2) >= DELTA) -- special case for cube going through another cube's corner
|
||||
and (0 < ti1 + DELTA
|
||||
or 0 == ti1 and ti2 > 0)
|
||||
then
|
||||
ti, nx, ny, nz = ti1, nx1, ny1, nz1
|
||||
overlaps = false
|
||||
end
|
||||
end
|
||||
|
||||
if not ti then
|
||||
return
|
||||
end
|
||||
|
||||
local tx, ty, tz
|
||||
|
||||
if overlaps then
|
||||
if dx == 0 and dy == 0 and dz == 0 then
|
||||
-- intersecting and not moving - use minimum displacement vector
|
||||
local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
|
||||
if abs(px) <= abs(py) and abs(px) <= abs(pz) then
|
||||
-- X axis has minimum displacement
|
||||
py, pz = 0, 0
|
||||
elseif abs(py) <= abs(pz) then
|
||||
-- Y axis has minimum displacement
|
||||
px, pz = 0, 0
|
||||
else
|
||||
-- Z axis has minimum displacement
|
||||
px, py = 0, 0
|
||||
end
|
||||
nx, ny, nz = sign(px), sign(py), sign(pz)
|
||||
tx = x1 + px
|
||||
ty = y1 + py
|
||||
tz = z1 + pz
|
||||
else
|
||||
-- intersecting and moving - move in the opposite direction
|
||||
local ti1, _
|
||||
ti1,_,nx,ny,nz = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, 1)
|
||||
if not ti1 then
|
||||
return
|
||||
end
|
||||
tx = x1 + dx * ti1
|
||||
ty = y1 + dy * ti1
|
||||
tz = z1 + dz * ti1
|
||||
end
|
||||
else -- tunnel
|
||||
tx = x1 + dx * ti
|
||||
ty = y1 + dy * ti
|
||||
tz = z1 + dz * ti
|
||||
end
|
||||
|
||||
return {
|
||||
overlaps = overlaps,
|
||||
ti = ti,
|
||||
move = {x = dx, y = dy, z = dz},
|
||||
normal = {x = nx, y = ny, z = nz},
|
||||
touch = {x = tx, y = ty, z = tz},
|
||||
itemCube = {x = x1, y = y1, z = z1, w = w1, h = h1, d = d1},
|
||||
otherCube = {x = x2, y = y2, z = z2, w = w2, h = h2, d = d2},
|
||||
}
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- Grid functions
|
||||
------------------------------------------
|
||||
|
||||
local function grid_toWorld(cellSize, cx, cy, cz)
|
||||
return (cx - 1) * cellSize,
|
||||
(cy - 1) * cellSize,
|
||||
(cz - 1) * cellSize
|
||||
end
|
||||
|
||||
local function grid_toCell(cellSize, x, y, z)
|
||||
return floor(x / cellSize) + 1,
|
||||
floor(y / cellSize) + 1,
|
||||
floor(z / cellSize) + 1
|
||||
end
|
||||
|
||||
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
|
||||
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
|
||||
-- It has been modified to include both cells when the ray "touches a grid corner",
|
||||
-- and with a different exit condition
|
||||
|
||||
local function grid_traverse_initStep(cellSize, ct, t1, t2)
|
||||
local v = t2 - t1
|
||||
if v > 0 then
|
||||
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
|
||||
elseif v < 0 then
|
||||
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
|
||||
else
|
||||
return 0, math.huge, math.huge
|
||||
end
|
||||
end
|
||||
|
||||
local function grid_traverse(cellSize, x1,y1,z1,x2,y2,z2, f)
|
||||
local cx1, cy1, cz1 = grid_toCell(cellSize, x1, y1, z1)
|
||||
local cx2, cy2, cz2 = grid_toCell(cellSize, x2, y2, z2)
|
||||
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
|
||||
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
|
||||
local stepZ, dz, tz = grid_traverse_initStep(cellSize, cz1, z1, z2)
|
||||
local cx, cy, cz = cx1, cy1, cz1
|
||||
|
||||
f(cx, cy, cz)
|
||||
|
||||
-- The default implementation had an infinite loop problem when
|
||||
-- approaching the last cell in some occassions. We finish iterating
|
||||
-- when we are *next* to the last cell
|
||||
while abs(cx - cx2) + abs(cy - cy2) + abs(cz - cz2) > 1 do
|
||||
if tx < ty and tx < tz then -- tx is smallest
|
||||
tx = tx + dx
|
||||
cx = cx + stepX
|
||||
f(cx, cy, cz)
|
||||
elseif ty < tz then -- ty is smallest
|
||||
-- Addition: include both cells when going through corners
|
||||
if tx == ty then
|
||||
f(cx + stepX, cy, cz)
|
||||
end
|
||||
ty = ty + dy
|
||||
cy = cy + stepY
|
||||
f(cx, cy, cz)
|
||||
else -- tz is smallest
|
||||
-- Addition: include both cells when going through corners
|
||||
if tx == tz then
|
||||
f(cx + stepX, cy, cz)
|
||||
end
|
||||
if ty == tz then
|
||||
f(cx, cy + stepY, cz)
|
||||
end
|
||||
tz = tz + dz
|
||||
cz = cz + stepZ
|
||||
f(cx, cy, cz)
|
||||
end
|
||||
end
|
||||
|
||||
-- If we have not arrived to the last cell, use it
|
||||
if cx ~= cx2 or cy ~= cy2 or cz ~= cz2 then
|
||||
f(cx2, cy2, cz2)
|
||||
end
|
||||
end
|
||||
|
||||
local function grid_toCellCube(cellSize, x,y,z,w,h,d)
|
||||
local cx,cy,cz = grid_toCell(cellSize, x, y, z)
|
||||
local cx2 = ceil((x + w) / cellSize)
|
||||
local cy2 = ceil((y + h) / cellSize)
|
||||
local cz2 = ceil((z + d) / cellSize)
|
||||
|
||||
return cx,
|
||||
cy,
|
||||
cz,
|
||||
cx2 - cx + 1,
|
||||
cy2 - cy + 1,
|
||||
cz2 - cz + 1
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- Responses
|
||||
------------------------------------------
|
||||
|
||||
local touch = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
return col.touch.x, col.touch.y, col.touch.z, {}, 0
|
||||
end
|
||||
|
||||
local cross = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
|
||||
return goalX, goalY, goalZ, cols, len
|
||||
end
|
||||
|
||||
local slide = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
goalX = goalX or x
|
||||
goalY = goalY or y
|
||||
goalZ = goalZ or z
|
||||
|
||||
local tch, move = col.touch, col.move
|
||||
if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
|
||||
if col.normal.x ~= 0 then
|
||||
goalX = tch.x
|
||||
end
|
||||
if col.normal.y ~= 0 then
|
||||
goalY = tch.y
|
||||
end
|
||||
if col.normal.z ~= 0 then
|
||||
goalZ = tch.z
|
||||
end
|
||||
end
|
||||
|
||||
col.slide = {x = goalX, y = goalY, z = goalZ}
|
||||
|
||||
x, y, z = tch.x, tch.y, tch.z
|
||||
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
|
||||
return goalX, goalY, goalZ, cols, len
|
||||
end
|
||||
|
||||
local bounce = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
goalX = goalX or x
|
||||
goalY = goalY or y
|
||||
goalZ = goalZ or z
|
||||
|
||||
local tch, move = col.touch, col.move
|
||||
local tx, ty, tz = tch.x, tch.y, tch.z
|
||||
local bx, by, bz = tx, ty, tz
|
||||
|
||||
if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
|
||||
local bnx = goalX - tx
|
||||
local bny = goalY - ty
|
||||
local bnz = goalZ - tz
|
||||
|
||||
if col.normal.x ~= 0 then
|
||||
bnx = -bnx
|
||||
end
|
||||
if col.normal.y ~= 0 then
|
||||
bny = -bny
|
||||
end
|
||||
if col.normal.z ~= 0 then
|
||||
bnz = -bnz
|
||||
end
|
||||
|
||||
bx = tx + bnx
|
||||
by = ty + bny
|
||||
bz = tz + bnz
|
||||
end
|
||||
|
||||
col.bounce = {x = bx, y = by, z = bz}
|
||||
x, y, z = tch.x, tch.y, tch.z
|
||||
goalX, goalY, goalZ = bx, by, bz
|
||||
|
||||
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
|
||||
|
||||
return goalX, goalY, goalZ, cols, len
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- World
|
||||
------------------------------------------
|
||||
|
||||
local World = {}
|
||||
local World_mt = {__index = World}
|
||||
|
||||
-- Private functions and methods
|
||||
|
||||
local function sortByWeight(a,b)
|
||||
return a.weight < b.weight
|
||||
end
|
||||
|
||||
local function sortByTiAndDistance(a,b)
|
||||
if a.ti == b.ti then
|
||||
local ir, ar, br = a.itemCube, a.otherCube, b.otherCube
|
||||
local ad = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, ar.x,ar.y,ar.z,ar.w,ar.h,ar.d)
|
||||
local bd = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, br.x,br.y,br.z,br.w,br.h,br.d)
|
||||
return ad < bd
|
||||
end
|
||||
return a.ti < b.ti
|
||||
end
|
||||
|
||||
local function addItemToCell(self, item, cx, cy, cz)
|
||||
self.cells[cz] = self.cells[cz] or {}
|
||||
self.cells[cz][cy] = self.cells[cz][cy] or setmetatable({}, {__mode = 'v'})
|
||||
if self.cells[cz][cy][cx] == nil then
|
||||
self.cells[cz][cy][cx] = {
|
||||
itemCount = 0,
|
||||
x = cx,
|
||||
y = cy,
|
||||
z = cz,
|
||||
items = setmetatable({}, {__mode = 'k'})
|
||||
}
|
||||
end
|
||||
|
||||
local cell = self.cells[cz][cy][cx]
|
||||
self.nonEmptyCells[cell] = true
|
||||
if not cell.items[item] then
|
||||
cell.items[item] = true
|
||||
cell.itemCount = cell.itemCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function removeItemFromCell(self, item, cx, cy, cz)
|
||||
if not self.cells[cz]
|
||||
or not self.cells[cz][cy]
|
||||
or not self.cells[cz][cy][cx]
|
||||
or not self.cells[cz][cy][cx].items[item]
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
local cell = self.cells[cz][cy][cx]
|
||||
cell.items[item] = nil
|
||||
|
||||
cell.itemCount = cell.itemCount - 1
|
||||
if cell.itemCount == 0 then
|
||||
self.nonEmptyCells[cell] = nil
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function getDictItemsInCellCube(self, cx,cy,cz, cw,ch,cd)
|
||||
local items_dict = {}
|
||||
|
||||
for z = cz, cz + cd - 1 do
|
||||
local plane = self.cells[z]
|
||||
if plane then
|
||||
for y = cy, cy + ch - 1 do
|
||||
local row = plane[y]
|
||||
if row then
|
||||
for x = cx, cx + cw - 1 do
|
||||
local cell = row[x]
|
||||
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
|
||||
for item,_ in pairs(cell.items) do
|
||||
items_dict[item] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items_dict
|
||||
end
|
||||
|
||||
local function getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
|
||||
local cells, cellsLen, visited = {}, 0, {}
|
||||
|
||||
grid_traverse(self.cellSize, x1,y1,z1,x2,y2,z2, function(cx, cy, cz)
|
||||
local plane = self.cells[cz]
|
||||
if not plane then
|
||||
return
|
||||
end
|
||||
|
||||
local row = plane[cy]
|
||||
if not row then
|
||||
return
|
||||
end
|
||||
|
||||
local cell = row[cx]
|
||||
if not cell or visited[cell] then
|
||||
return
|
||||
end
|
||||
|
||||
visited[cell] = true
|
||||
cellsLen = cellsLen + 1
|
||||
cells[cellsLen] = cell
|
||||
end)
|
||||
|
||||
return cells, cellsLen
|
||||
end
|
||||
|
||||
local function getInfoAboutItemsTouchedBySegment(self, x1,y1,z1, x2,y2,z2, filter)
|
||||
local cells, len = getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
|
||||
local cell, cube, x,y,z,w,h,d, ti1, ti2, tii0,tii1
|
||||
local visited, itemInfo, itemInfoLen = {}, {}, 0
|
||||
|
||||
for i = 1, len do
|
||||
cell = cells[i]
|
||||
for item in pairs(cell.items) do
|
||||
if not visited[item] then
|
||||
visited[item] = true
|
||||
if (not filter or filter(item)) then
|
||||
cube = self.cubes[item]
|
||||
x, y, z, w, h, d = cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
|
||||
|
||||
ti1, ti2 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, 0, 1)
|
||||
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
|
||||
-- the sorting is according to the t of an infinite line, not the segment
|
||||
tii0, tii1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, -math.huge, math.huge)
|
||||
itemInfoLen = itemInfoLen + 1
|
||||
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0, tii1)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(itemInfo, sortByWeight)
|
||||
|
||||
return itemInfo, itemInfoLen
|
||||
end
|
||||
|
||||
local function getResponseByName(self, name)
|
||||
local response = self.responses[name]
|
||||
if not response then
|
||||
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
|
||||
end
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
|
||||
-- Misc Public Methods
|
||||
|
||||
function World:addResponse(name, response)
|
||||
self.responses[name] = response
|
||||
end
|
||||
|
||||
function World:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
|
||||
local cols, len = {}, 0
|
||||
|
||||
filter = filter or defaultFilter
|
||||
|
||||
local visited = {[item] = true}
|
||||
local visitedFilter = function(itm, other)
|
||||
if visited[other] then
|
||||
return false
|
||||
end
|
||||
return filter(itm, other)
|
||||
end
|
||||
|
||||
local projected_cols, projected_len = self:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, visitedFilter)
|
||||
|
||||
while projected_len > 0 do
|
||||
local col = projected_cols[1]
|
||||
len = len + 1
|
||||
cols[len] = col
|
||||
|
||||
visited[col.other] = true
|
||||
|
||||
local response = getResponseByName(self, col.type)
|
||||
|
||||
goalX, goalY, goalZ, projected_cols, projected_len = response(
|
||||
self,
|
||||
col,
|
||||
x, y, z, w, h, d,
|
||||
goalX, goalY, goalZ,
|
||||
visitedFilter
|
||||
)
|
||||
end
|
||||
|
||||
return goalX, goalY, goalZ, cols, len
|
||||
end
|
||||
|
||||
function World:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
|
||||
assertIsCube(x, y, z, w, h, d)
|
||||
|
||||
goalX = goalX or x
|
||||
goalY = goalY or y
|
||||
goalZ = goalZ or z
|
||||
filter = filter or defaultFilter
|
||||
|
||||
local collisions, len = {}, 0
|
||||
|
||||
local visited = {}
|
||||
if item ~= nil then
|
||||
visited[item] = true
|
||||
end
|
||||
|
||||
-- This could probably be done with less cells using a polygon raster over the cells instead of a
|
||||
-- bounding cube of the whole movement. Conditional to building a queryPolygon method
|
||||
local tx = min(goalX, x)
|
||||
local ty = min(goalY, y)
|
||||
local tz = min(goalZ, z)
|
||||
local tx2 = max(goalX + w, x + w)
|
||||
local ty2 = max(goalY + h, y + h)
|
||||
local tz2 = max(goalZ + d, z + d)
|
||||
local tw = tx2 - tx
|
||||
local th = ty2 - ty
|
||||
local td = tz2 - tz
|
||||
|
||||
local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, tx,ty,tz, tw,th,td)
|
||||
|
||||
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
|
||||
|
||||
for other,_ in pairs(dictItemsInCellCube) do
|
||||
if not visited[other] then
|
||||
visited[other] = true
|
||||
|
||||
local responseName = filter(item, other)
|
||||
if responseName then
|
||||
local ox,oy,oz,ow,oh,od = self:getCube(other)
|
||||
local col = cube_detectCollision(x,y,z,w,h,d, ox,oy,oz,ow,oh,od, goalX, goalY, goalZ)
|
||||
|
||||
if col then
|
||||
col.other = other
|
||||
col.item = item
|
||||
col.type = responseName
|
||||
|
||||
len = len + 1
|
||||
collisions[len] = col
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(collisions, sortByTiAndDistance)
|
||||
|
||||
return collisions, len
|
||||
end
|
||||
|
||||
function World:countCells()
|
||||
local count = 0
|
||||
|
||||
for _, plane in pairs(self.cells) do
|
||||
for _, row in pairs(plane) do
|
||||
for _,_ in pairs(row) do
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
function World:hasItem(item)
|
||||
return not not self.cubes[item]
|
||||
end
|
||||
|
||||
function World:getItems()
|
||||
local items, len = {}, 0
|
||||
for item,_ in pairs(self.cubes) do
|
||||
len = len + 1
|
||||
items[len] = item
|
||||
end
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:countItems()
|
||||
local len = 0
|
||||
for _ in pairs(self.cubes) do len = len + 1 end
|
||||
return len
|
||||
end
|
||||
|
||||
function World:getCube(item)
|
||||
local cube = self.cubes[item]
|
||||
if not cube then
|
||||
error('Item ' .. tostring(item) .. ' must be added to the world before getting its cube. Use world:add(item, x,y,z,w,h,d) to add it first.')
|
||||
end
|
||||
|
||||
return cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
|
||||
end
|
||||
|
||||
function World:toWorld(cx, cy, cz)
|
||||
return grid_toWorld(self.cellSize, cx, cy, cz)
|
||||
end
|
||||
|
||||
function World:toCell(x,y,z)
|
||||
return grid_toCell(self.cellSize, x, y, z)
|
||||
end
|
||||
|
||||
|
||||
-- Query methods
|
||||
|
||||
function World:queryCube(x,y,z,w,h,d, filter)
|
||||
assertIsCube(x,y,z,w,h,d)
|
||||
|
||||
local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
|
||||
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
|
||||
|
||||
local items, len = {}, 0
|
||||
|
||||
local cube
|
||||
for item,_ in pairs(dictItemsInCellCube) do
|
||||
cube = self.cubes[item]
|
||||
if (not filter or filter(item))
|
||||
and cube_isIntersecting(x,y,z,w,h,d, cube.x, cube.y, cube.z, cube.w, cube.h, cube.d)
|
||||
then
|
||||
len = len + 1
|
||||
items[len] = item
|
||||
end
|
||||
end
|
||||
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:queryPoint(x,y,z, filter)
|
||||
local cx,cy,cz = self:toCell(x,y,z)
|
||||
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz, 1,1,1)
|
||||
|
||||
local items, len = {}, 0
|
||||
|
||||
local cube
|
||||
for item,_ in pairs(dictItemsInCellCube) do
|
||||
cube = self.cubes[item]
|
||||
if (not filter or filter(item))
|
||||
and cube_containsPoint(cube.x, cube.y, cube.z, cube.w, cube.h, cube.d, x, y, z)
|
||||
then
|
||||
len = len + 1
|
||||
items[len] = item
|
||||
end
|
||||
end
|
||||
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:querySegment(x1, y1, z1, x2, y2, z2, filter)
|
||||
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
|
||||
local items = {}
|
||||
for i = 1, len do
|
||||
items[i] = itemInfo[i].item
|
||||
end
|
||||
|
||||
return items, len
|
||||
end
|
||||
|
||||
-- function World:querySegmentWithCoords(x1, y1, z1, x2, y2, z2, filter)
|
||||
-- local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
|
||||
-- local dx, dy, dz = x2 - x1, y2 - y1, z2 - z1
|
||||
-- local info, ti1, ti2
|
||||
-- for i=1, len do
|
||||
-- info = itemInfo[i]
|
||||
-- ti1 = info.ti1
|
||||
-- ti2 = info.ti2
|
||||
|
||||
-- info.weight = nil
|
||||
-- info.x1 = x1 + dx * ti1
|
||||
-- info.y1 = y1 + dy * ti1
|
||||
-- info.x2 = x1 + dx * ti2
|
||||
-- info.y2 = y1 + dy * ti2
|
||||
-- end
|
||||
-- return itemInfo, len
|
||||
-- end
|
||||
|
||||
|
||||
--- Main methods
|
||||
|
||||
function World:add(item, x,y,z,w,h,d)
|
||||
local cube = self.cubes[item]
|
||||
if cube then
|
||||
error('Item ' .. tostring(item) .. ' added to the world twice.')
|
||||
end
|
||||
assertIsCube(x,y,z,w,h,d)
|
||||
|
||||
self.cubes[item] = {x=x,y=y,z=z,w=w,h=h,d=d}
|
||||
|
||||
local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
|
||||
for cz = cs, cs + cd - 1 do
|
||||
for cy = ct, ct + ch - 1 do
|
||||
for cx = cl, cl + cw - 1 do
|
||||
addItemToCell(self, item, cx, cy, cz)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return item
|
||||
end
|
||||
|
||||
function World:remove(item)
|
||||
local x,y,z,w,h,d = self:getCube(item)
|
||||
|
||||
self.cubes[item] = nil
|
||||
local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
|
||||
for cz = cs, cs + cd - 1 do
|
||||
for cy = ct, ct + ch - 1 do
|
||||
for cx = cl, cl + cw - 1 do
|
||||
removeItemFromCell(self, item, cx, cy, cz)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function World:update(item, x2,y2,z2,w2,h2,d2)
|
||||
local x1,y1,z1, w1,h1,d1 = self:getCube(item)
|
||||
w2 = w2 or w1
|
||||
h2 = h2 or h1
|
||||
d2 = d2 or d1
|
||||
assertIsCube(x2,y2,z2,w2,h2,d2)
|
||||
|
||||
if x1 == x2 and y1 == y2 and z1 == z2 and w1 == w2 and h1 == h2 and d1 == d2 then
|
||||
return
|
||||
end
|
||||
|
||||
local cl1,ct1,cs1,cw1,ch1,cd1 = grid_toCellCube(self.cellSize, x1,y1,z1, w1,h1,d1)
|
||||
local cl2,ct2,cs2,cw2,ch2,cd2 = grid_toCellCube(self.cellSize, x2,y2,z2, w2,h2,d2)
|
||||
|
||||
if cl1 ~= cl2 or ct1 ~= ct2 or cs1 ~= cs2 or cw1 ~= cw2 or ch1 ~= ch2 or cd1 ~= cd2 then
|
||||
local cr1 = cl1 + cw1 - 1
|
||||
local cr2 = cl2 + cw2 - 1
|
||||
local cb1 = ct1 + ch1 - 1
|
||||
local cb2 = ct2 + ch2 - 1
|
||||
local css1 = cs1 + cd1 - 1
|
||||
local css2 = cs2 + cd2 - 1
|
||||
local cyOut, czOut
|
||||
|
||||
for cz = cs1, css1 do
|
||||
czOut = cz < cs2 or cz > css2
|
||||
for cy = ct1, cb1 do
|
||||
cyOut = cy < ct2 or cy > cb2
|
||||
for cx = cl1, cr1 do
|
||||
if czOut or cyOut or cx < cl2 or cx > cr2 then
|
||||
removeItemFromCell(self, item, cx, cy, cz)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for cz = cs2, css2 do
|
||||
czOut = cz < cs1 or cz > css1
|
||||
for cy = ct2, cb2 do
|
||||
cyOut = cy < ct1 or cy > cb1
|
||||
for cx = cl2, cr2 do
|
||||
if czOut or cyOut or cx < cl1 or cx > cr1 then
|
||||
addItemToCell(self, item, cx, cy, cz)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local cube = self.cubes[item]
|
||||
cube.x, cube.y, cube.z, cube.w, cube.h, cube.d = x2, y2, z2, w2, h2, d2
|
||||
end
|
||||
|
||||
function World:move(item, goalX, goalY, goalZ, filter)
|
||||
local actualX, actualY, actualZ, cols, len = self:check(item, goalX, goalY, goalZ, filter)
|
||||
|
||||
self:update(item, actualX, actualY, actualZ)
|
||||
|
||||
return actualX, actualY, actualZ, cols, len
|
||||
end
|
||||
|
||||
function World:check(item, goalX, goalY, goalZ, filter)
|
||||
local x,y,z,w,h,d = self:getCube(item)
|
||||
|
||||
return self:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
|
||||
end
|
||||
|
||||
|
||||
-- Public library functions
|
||||
|
||||
bump.newWorld = function(cellSize)
|
||||
cellSize = cellSize or 64
|
||||
assertIsPositiveNumber(cellSize, 'cellSize')
|
||||
local world = setmetatable({
|
||||
cellSize = cellSize,
|
||||
cubes = {},
|
||||
cells = {},
|
||||
nonEmptyCells = {},
|
||||
responses = {},
|
||||
}, World_mt)
|
||||
|
||||
world:addResponse('touch', touch)
|
||||
world:addResponse('cross', cross)
|
||||
world:addResponse('slide', slide)
|
||||
world:addResponse('bounce', bounce)
|
||||
|
||||
return world
|
||||
end
|
||||
|
||||
bump.cube = {
|
||||
getNearestCorner = cube_getNearestCorner,
|
||||
getSegmentIntersectionIndices = cube_getSegmentIntersectionIndices,
|
||||
getDiff = cube_getDiff,
|
||||
containsPoint = cube_containsPoint,
|
||||
isIntersecting = cube_isIntersecting,
|
||||
getCubeDistance = cube_getCubeDistance,
|
||||
detectCollision = cube_detectCollision
|
||||
}
|
||||
|
||||
bump.responses = {
|
||||
touch = touch,
|
||||
cross = cross,
|
||||
slide = slide,
|
||||
bounce = bounce
|
||||
}
|
||||
|
||||
return bump
|
|
@ -1,769 +0,0 @@
|
|||
local bump = {
|
||||
_VERSION = 'bump v3.1.7',
|
||||
_URL = 'https://github.com/kikito/bump.lua',
|
||||
_DESCRIPTION = 'A collision detection library for Lua',
|
||||
_LICENSE = [[
|
||||
MIT LICENSE
|
||||
Copyright (c) 2014 Enrique García Cota
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
}
|
||||
|
||||
------------------------------------------
|
||||
-- Auxiliary functions
|
||||
------------------------------------------
|
||||
local DELTA = 1e-10 -- floating-point margin of error
|
||||
|
||||
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
|
||||
|
||||
local function sign(x)
|
||||
if x > 0 then return 1 end
|
||||
if x == 0 then return 0 end
|
||||
return -1
|
||||
end
|
||||
|
||||
local function nearest(x, a, b)
|
||||
if abs(a - x) < abs(b - x) then return a else return b end
|
||||
end
|
||||
|
||||
local function assertType(desiredType, value, name)
|
||||
if type(value) ~= desiredType then
|
||||
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
|
||||
end
|
||||
end
|
||||
|
||||
local function assertIsPositiveNumber(value, name)
|
||||
if type(value) ~= 'number' or value <= 0 then
|
||||
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
|
||||
end
|
||||
end
|
||||
|
||||
local function assertIsRect(x,y,w,h)
|
||||
assertType('number', x, 'x')
|
||||
assertType('number', y, 'y')
|
||||
assertIsPositiveNumber(w, 'w')
|
||||
assertIsPositiveNumber(h, 'h')
|
||||
end
|
||||
|
||||
local defaultFilter = function()
|
||||
return 'slide'
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- Rectangle functions
|
||||
------------------------------------------
|
||||
|
||||
local function rect_getNearestCorner(x,y,w,h, px, py)
|
||||
return nearest(px, x, x+w), nearest(py, y, y+h)
|
||||
end
|
||||
|
||||
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
|
||||
-- the normals of the sides where the segment intersects.
|
||||
-- Returns nil if the segment never touches the rect
|
||||
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
|
||||
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
|
||||
ti1, ti2 = ti1 or 0, ti2 or 1
|
||||
local dx, dy = x2-x1, y2-y1
|
||||
local nx, ny
|
||||
local nx1, ny1, nx2, ny2 = 0,0,0,0
|
||||
local p, q, r
|
||||
|
||||
for side = 1,4 do
|
||||
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
|
||||
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
|
||||
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
|
||||
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
|
||||
end
|
||||
|
||||
if p == 0 then
|
||||
if q <= 0 then return nil end
|
||||
else
|
||||
r = q / p
|
||||
if p < 0 then
|
||||
if r > ti2 then return nil
|
||||
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
|
||||
end
|
||||
else -- p > 0
|
||||
if r < ti1 then return nil
|
||||
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ti1,ti2, nx1,ny1, nx2,ny2
|
||||
end
|
||||
|
||||
-- Calculates the minkowsky difference between 2 rects, which is another rect
|
||||
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||
return x2 - x1 - w1,
|
||||
y2 - y1 - h1,
|
||||
w1 + w2,
|
||||
h1 + h2
|
||||
end
|
||||
|
||||
local function rect_containsPoint(x,y,w,h, px,py)
|
||||
return px - x > DELTA and py - y > DELTA and
|
||||
x + w - px > DELTA and y + h - py > DELTA
|
||||
end
|
||||
|
||||
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||
return x1 < x2+w2 and x2 < x1+w1 and
|
||||
y1 < y2+h2 and y2 < y1+h1
|
||||
end
|
||||
|
||||
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||
local dx = x1 - x2 + (w1 - w2)/2
|
||||
local dy = y1 - y2 + (h1 - h2)/2
|
||||
return dx*dx + dy*dy
|
||||
end
|
||||
|
||||
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
|
||||
goalX = goalX or x1
|
||||
goalY = goalY or y1
|
||||
|
||||
local dx, dy = goalX - x1, goalY - y1
|
||||
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
|
||||
|
||||
local overlaps, ti, nx, ny
|
||||
|
||||
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
|
||||
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
|
||||
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
|
||||
ti = -wi * hi -- ti is the negative area of intersection
|
||||
overlaps = true
|
||||
else
|
||||
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
|
||||
|
||||
-- item tunnels into other
|
||||
if ti1
|
||||
and ti1 < 1
|
||||
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
|
||||
and (0 < ti1 + DELTA
|
||||
or 0 == ti1 and ti2 > 0)
|
||||
then
|
||||
ti, nx, ny = ti1, nx1, ny1
|
||||
overlaps = false
|
||||
end
|
||||
end
|
||||
|
||||
if not ti then return end
|
||||
|
||||
local tx, ty
|
||||
|
||||
if overlaps then
|
||||
if dx == 0 and dy == 0 then
|
||||
-- intersecting and not moving - use minimum displacement vector
|
||||
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
|
||||
if abs(px) < abs(py) then py = 0 else px = 0 end
|
||||
nx, ny = sign(px), sign(py)
|
||||
tx, ty = x1 + px, y1 + py
|
||||
else
|
||||
-- intersecting and moving - move in the opposite direction
|
||||
local ti1, _
|
||||
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
|
||||
if not ti1 then return end
|
||||
tx, ty = x1 + dx * ti1, y1 + dy * ti1
|
||||
end
|
||||
else -- tunnel
|
||||
tx, ty = x1 + dx * ti, y1 + dy * ti
|
||||
end
|
||||
|
||||
return {
|
||||
overlaps = overlaps,
|
||||
ti = ti,
|
||||
move = {x = dx, y = dy},
|
||||
normal = {x = nx, y = ny},
|
||||
touch = {x = tx, y = ty},
|
||||
itemRect = {x = x1, y = y1, w = w1, h = h1},
|
||||
otherRect = {x = x2, y = y2, w = w2, h = h2}
|
||||
}
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- Grid functions
|
||||
------------------------------------------
|
||||
|
||||
local function grid_toWorld(cellSize, cx, cy)
|
||||
return (cx - 1)*cellSize, (cy-1)*cellSize
|
||||
end
|
||||
|
||||
local function grid_toCell(cellSize, x, y)
|
||||
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
|
||||
end
|
||||
|
||||
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
|
||||
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
|
||||
-- It has been modified to include both cells when the ray "touches a grid corner",
|
||||
-- and with a different exit condition
|
||||
|
||||
local function grid_traverse_initStep(cellSize, ct, t1, t2)
|
||||
local v = t2 - t1
|
||||
if v > 0 then
|
||||
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
|
||||
elseif v < 0 then
|
||||
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
|
||||
else
|
||||
return 0, math.huge, math.huge
|
||||
end
|
||||
end
|
||||
|
||||
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
|
||||
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
|
||||
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
|
||||
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
|
||||
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
|
||||
local cx,cy = cx1,cy1
|
||||
|
||||
f(cx, cy)
|
||||
|
||||
-- The default implementation had an infinite loop problem when
|
||||
-- approaching the last cell in some occassions. We finish iterating
|
||||
-- when we are *next* to the last cell
|
||||
while abs(cx - cx2) + abs(cy - cy2) > 1 do
|
||||
if tx < ty then
|
||||
tx, cx = tx + dx, cx + stepX
|
||||
f(cx, cy)
|
||||
else
|
||||
-- Addition: include both cells when going through corners
|
||||
if tx == ty then f(cx + stepX, cy) end
|
||||
ty, cy = ty + dy, cy + stepY
|
||||
f(cx, cy)
|
||||
end
|
||||
end
|
||||
|
||||
-- If we have not arrived to the last cell, use it
|
||||
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
|
||||
|
||||
end
|
||||
|
||||
local function grid_toCellRect(cellSize, x,y,w,h)
|
||||
local cx,cy = grid_toCell(cellSize, x, y)
|
||||
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
|
||||
return cx, cy, cr - cx + 1, cb - cy + 1
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- Responses
|
||||
------------------------------------------
|
||||
|
||||
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||
return col.touch.x, col.touch.y, {}, 0
|
||||
end
|
||||
|
||||
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
|
||||
return goalX, goalY, cols, len
|
||||
end
|
||||
|
||||
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||
goalX = goalX or x
|
||||
goalY = goalY or y
|
||||
|
||||
local tch, move = col.touch, col.move
|
||||
if move.x ~= 0 or move.y ~= 0 then
|
||||
if col.normal.x ~= 0 then
|
||||
goalX = tch.x
|
||||
else
|
||||
goalY = tch.y
|
||||
end
|
||||
end
|
||||
|
||||
col.slide = {x = goalX, y = goalY}
|
||||
|
||||
x,y = tch.x, tch.y
|
||||
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
|
||||
return goalX, goalY, cols, len
|
||||
end
|
||||
|
||||
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
|
||||
goalX = goalX or x
|
||||
goalY = goalY or y
|
||||
|
||||
local tch, move = col.touch, col.move
|
||||
local tx, ty = tch.x, tch.y
|
||||
|
||||
local bx, by = tx, ty
|
||||
|
||||
if move.x ~= 0 or move.y ~= 0 then
|
||||
local bnx, bny = goalX - tx, goalY - ty
|
||||
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
|
||||
bx, by = tx + bnx, ty + bny
|
||||
end
|
||||
|
||||
col.bounce = {x = bx, y = by}
|
||||
x,y = tch.x, tch.y
|
||||
goalX, goalY = bx, by
|
||||
|
||||
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
|
||||
return goalX, goalY, cols, len
|
||||
end
|
||||
|
||||
------------------------------------------
|
||||
-- World
|
||||
------------------------------------------
|
||||
|
||||
local World = {}
|
||||
local World_mt = {__index = World}
|
||||
|
||||
-- Private functions and methods
|
||||
|
||||
local function sortByWeight(a,b) return a.weight < b.weight end
|
||||
|
||||
local function sortByTiAndDistance(a,b)
|
||||
if a.ti == b.ti then
|
||||
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
|
||||
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
|
||||
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
|
||||
return ad < bd
|
||||
end
|
||||
return a.ti < b.ti
|
||||
end
|
||||
|
||||
local function addItemToCell(self, item, cx, cy)
|
||||
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
|
||||
local row = self.rows[cy]
|
||||
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
|
||||
local cell = row[cx]
|
||||
self.nonEmptyCells[cell] = true
|
||||
if not cell.items[item] then
|
||||
cell.items[item] = true
|
||||
cell.itemCount = cell.itemCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function removeItemFromCell(self, item, cx, cy)
|
||||
local row = self.rows[cy]
|
||||
if not row or not row[cx] or not row[cx].items[item] then return false end
|
||||
|
||||
local cell = row[cx]
|
||||
cell.items[item] = nil
|
||||
cell.itemCount = cell.itemCount - 1
|
||||
if cell.itemCount == 0 then
|
||||
self.nonEmptyCells[cell] = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
|
||||
local items_dict = {}
|
||||
for cy=ct,ct+ch-1 do
|
||||
local row = self.rows[cy]
|
||||
if row then
|
||||
for cx=cl,cl+cw-1 do
|
||||
local cell = row[cx]
|
||||
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
|
||||
for item,_ in pairs(cell.items) do
|
||||
items_dict[item] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items_dict
|
||||
end
|
||||
|
||||
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
|
||||
|
||||
local cells, cellsLen, visited = {}, 0, {}
|
||||
|
||||
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
|
||||
local row = self.rows[cy]
|
||||
if not row then return end
|
||||
local cell = row[cx]
|
||||
if not cell or visited[cell] then return end
|
||||
|
||||
visited[cell] = true
|
||||
cellsLen = cellsLen + 1
|
||||
cells[cellsLen] = cell
|
||||
end)
|
||||
|
||||
return cells, cellsLen
|
||||
end
|
||||
|
||||
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
|
||||
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
|
||||
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
|
||||
local visited, itemInfo, itemInfoLen = {},{},0
|
||||
for i=1,len do
|
||||
cell = cells[i]
|
||||
for item in pairs(cell.items) do
|
||||
if not visited[item] then
|
||||
visited[item] = true
|
||||
if (not filter or filter(item)) then
|
||||
rect = self.rects[item]
|
||||
l,t,w,h = rect.x,rect.y,rect.w,rect.h
|
||||
|
||||
ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
|
||||
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
|
||||
-- the sorting is according to the t of an infinite line, not the segment
|
||||
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
|
||||
itemInfoLen = itemInfoLen + 1
|
||||
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(itemInfo, sortByWeight)
|
||||
return itemInfo, itemInfoLen
|
||||
end
|
||||
|
||||
local function getResponseByName(self, name)
|
||||
local response = self.responses[name]
|
||||
if not response then
|
||||
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
|
||||
end
|
||||
return response
|
||||
end
|
||||
|
||||
|
||||
-- Misc Public Methods
|
||||
|
||||
function World:addResponse(name, response)
|
||||
self.responses[name] = response
|
||||
end
|
||||
|
||||
function World:project(item, x,y,w,h, goalX, goalY, filter)
|
||||
assertIsRect(x,y,w,h)
|
||||
|
||||
goalX = goalX or x
|
||||
goalY = goalY or y
|
||||
filter = filter or defaultFilter
|
||||
|
||||
local collisions, len = {}, 0
|
||||
|
||||
local visited = {}
|
||||
if item ~= nil then visited[item] = true end
|
||||
|
||||
-- This could probably be done with less cells using a polygon raster over the cells instead of a
|
||||
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
|
||||
local tl, tt = min(goalX, x), min(goalY, y)
|
||||
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
|
||||
local tw, th = tr-tl, tb-tt
|
||||
|
||||
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
|
||||
|
||||
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
|
||||
|
||||
for other,_ in pairs(dictItemsInCellRect) do
|
||||
if not visited[other] then
|
||||
visited[other] = true
|
||||
|
||||
local responseName = filter(item, other)
|
||||
if responseName then
|
||||
local ox,oy,ow,oh = self:getRect(other)
|
||||
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
|
||||
|
||||
if col then
|
||||
col.other = other
|
||||
col.item = item
|
||||
col.type = responseName
|
||||
|
||||
len = len + 1
|
||||
collisions[len] = col
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(collisions, sortByTiAndDistance)
|
||||
|
||||
return collisions, len
|
||||
end
|
||||
|
||||
function World:countCells()
|
||||
local count = 0
|
||||
for _,row in pairs(self.rows) do
|
||||
for _,_ in pairs(row) do
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function World:hasItem(item)
|
||||
return not not self.rects[item]
|
||||
end
|
||||
|
||||
function World:getItems()
|
||||
local items, len = {}, 0
|
||||
for item,_ in pairs(self.rects) do
|
||||
len = len + 1
|
||||
items[len] = item
|
||||
end
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:countItems()
|
||||
local len = 0
|
||||
for _ in pairs(self.rects) do len = len + 1 end
|
||||
return len
|
||||
end
|
||||
|
||||
function World:getRect(item)
|
||||
local rect = self.rects[item]
|
||||
if not rect then
|
||||
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.')
|
||||
end
|
||||
return rect.x, rect.y, rect.w, rect.h
|
||||
end
|
||||
|
||||
function World:toWorld(cx, cy)
|
||||
return grid_toWorld(self.cellSize, cx, cy)
|
||||
end
|
||||
|
||||
function World:toCell(x,y)
|
||||
return grid_toCell(self.cellSize, x, y)
|
||||
end
|
||||
|
||||
|
||||
--- Query methods
|
||||
|
||||
function World:queryRect(x,y,w,h, filter)
|
||||
|
||||
assertIsRect(x,y,w,h)
|
||||
|
||||
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
|
||||
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
|
||||
|
||||
local items, len = {}, 0
|
||||
|
||||
local rect
|
||||
for item,_ in pairs(dictItemsInCellRect) do
|
||||
rect = self.rects[item]
|
||||
if (not filter or filter(item))
|
||||
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
|
||||
then
|
||||
len = len + 1
|
||||
items[len] = item
|
||||
end
|
||||
end
|
||||
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:queryPoint(x,y, filter)
|
||||
local cx,cy = self:toCell(x,y)
|
||||
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
|
||||
|
||||
local items, len = {}, 0
|
||||
|
||||
local rect
|
||||
for item,_ in pairs(dictItemsInCellRect) do
|
||||
rect = self.rects[item]
|
||||
if (not filter or filter(item))
|
||||
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
|
||||
then
|
||||
len = len + 1
|
||||
items[len] = item
|
||||
end
|
||||
end
|
||||
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:querySegment(x1, y1, x2, y2, filter)
|
||||
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||
local items = {}
|
||||
for i=1, len do
|
||||
items[i] = itemInfo[i].item
|
||||
end
|
||||
return items, len
|
||||
end
|
||||
|
||||
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
|
||||
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||
local dx, dy = x2-x1, y2-y1
|
||||
local info, ti1, ti2
|
||||
for i=1, len do
|
||||
info = itemInfo[i]
|
||||
ti1 = info.ti1
|
||||
ti2 = info.ti2
|
||||
|
||||
info.weight = nil
|
||||
info.x1 = x1 + dx * ti1
|
||||
info.y1 = y1 + dy * ti1
|
||||
info.x2 = x1 + dx * ti2
|
||||
info.y2 = y1 + dy * ti2
|
||||
end
|
||||
return itemInfo, len
|
||||
end
|
||||
|
||||
|
||||
--- Main methods
|
||||
|
||||
function World:add(item, x,y,w,h)
|
||||
local rect = self.rects[item]
|
||||
if rect then
|
||||
error('Item ' .. tostring(item) .. ' added to the world twice.')
|
||||
end
|
||||
assertIsRect(x,y,w,h)
|
||||
|
||||
self.rects[item] = {x=x,y=y,w=w,h=h}
|
||||
|
||||
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
|
||||
for cy = ct, ct+ch-1 do
|
||||
for cx = cl, cl+cw-1 do
|
||||
addItemToCell(self, item, cx, cy)
|
||||
end
|
||||
end
|
||||
|
||||
return item
|
||||
end
|
||||
|
||||
function World:remove(item)
|
||||
local x,y,w,h = self:getRect(item)
|
||||
|
||||
self.rects[item] = nil
|
||||
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
|
||||
for cy = ct, ct+ch-1 do
|
||||
for cx = cl, cl+cw-1 do
|
||||
removeItemFromCell(self, item, cx, cy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function World:update(item, x2,y2,w2,h2)
|
||||
local x1,y1,w1,h1 = self:getRect(item)
|
||||
w2,h2 = w2 or w1, h2 or h1
|
||||
assertIsRect(x2,y2,w2,h2)
|
||||
|
||||
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
|
||||
|
||||
local cellSize = self.cellSize
|
||||
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
|
||||
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
|
||||
|
||||
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
|
||||
|
||||
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
|
||||
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
|
||||
local cyOut
|
||||
|
||||
for cy = ct1, cb1 do
|
||||
cyOut = cy < ct2 or cy > cb2
|
||||
for cx = cl1, cr1 do
|
||||
if cyOut or cx < cl2 or cx > cr2 then
|
||||
removeItemFromCell(self, item, cx, cy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for cy = ct2, cb2 do
|
||||
cyOut = cy < ct1 or cy > cb1
|
||||
for cx = cl2, cr2 do
|
||||
if cyOut or cx < cl1 or cx > cr1 then
|
||||
addItemToCell(self, item, cx, cy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local rect = self.rects[item]
|
||||
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
function World:move(item, goalX, goalY, filter)
|
||||
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
|
||||
|
||||
self:update(item, actualX, actualY)
|
||||
|
||||
return actualX, actualY, cols, len
|
||||
end
|
||||
|
||||
function World:check(item, goalX, goalY, filter)
|
||||
filter = filter or defaultFilter
|
||||
|
||||
local visited = {[item] = true}
|
||||
local visitedFilter = function(itm, other)
|
||||
if visited[other] then return false end
|
||||
return filter(itm, other)
|
||||
end
|
||||
|
||||
local cols, len = {}, 0
|
||||
|
||||
local x,y,w,h = self:getRect(item)
|
||||
|
||||
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
|
||||
|
||||
while projected_len > 0 do
|
||||
local col = projected_cols[1]
|
||||
len = len + 1
|
||||
cols[len] = col
|
||||
|
||||
visited[col.other] = true
|
||||
|
||||
local response = getResponseByName(self, col.type)
|
||||
|
||||
goalX, goalY, projected_cols, projected_len = response(
|
||||
self,
|
||||
col,
|
||||
x, y, w, h,
|
||||
goalX, goalY,
|
||||
visitedFilter
|
||||
)
|
||||
end
|
||||
|
||||
return goalX, goalY, cols, len
|
||||
end
|
||||
|
||||
|
||||
-- Public library functions
|
||||
|
||||
bump.newWorld = function(cellSize)
|
||||
cellSize = cellSize or 64
|
||||
assertIsPositiveNumber(cellSize, 'cellSize')
|
||||
local world = setmetatable({
|
||||
cellSize = cellSize,
|
||||
rects = {},
|
||||
rows = {},
|
||||
nonEmptyCells = {},
|
||||
responses = {}
|
||||
}, World_mt)
|
||||
|
||||
world:addResponse('touch', touch)
|
||||
world:addResponse('cross', cross)
|
||||
world:addResponse('slide', slide)
|
||||
world:addResponse('bounce', bounce)
|
||||
|
||||
return world
|
||||
end
|
||||
|
||||
bump.rect = {
|
||||
getNearestCorner = rect_getNearestCorner,
|
||||
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
|
||||
getDiff = rect_getDiff,
|
||||
containsPoint = rect_containsPoint,
|
||||
isIntersecting = rect_isIntersecting,
|
||||
getSquareDistance = rect_getSquareDistance,
|
||||
detectCollision = rect_detectCollision
|
||||
}
|
||||
|
||||
bump.responses = {
|
||||
touch = touch,
|
||||
cross = cross,
|
||||
slide = slide,
|
||||
bounce = bounce
|
||||
}
|
||||
|
||||
return bump
|
|
@ -1,84 +0,0 @@
|
|||
-- See: https://github.com/bungle/lua-resty-tsort
|
||||
--
|
||||
-- Copyright (c) 2016, Aapo Talvensaari
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- Redistribution and use in source and binary forms, with or without modification,
|
||||
-- are permitted provided that the following conditions are met:
|
||||
--
|
||||
-- * Redistributions of source code must retain the above copyright notice, this
|
||||
-- list of conditions and the following disclaimer.
|
||||
--
|
||||
-- * Redistributions in binary form must reproduce the above copyright notice, this
|
||||
-- list of conditions and the following disclaimer in the documentation and/or
|
||||
-- other materials provided with the distribution.
|
||||
--
|
||||
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
-- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
-- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
-- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
local setmetatable = setmetatable
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local function visit(k, n, m, s)
|
||||
if m[k] == 0 then return 1 end
|
||||
if m[k] == 1 then return end
|
||||
m[k] = 0
|
||||
local f = n[k]
|
||||
for i=1, #f do
|
||||
if visit(f[i], n, m, s) then return 1 end
|
||||
end
|
||||
m[k] = 1
|
||||
s[#s+1] = k
|
||||
end
|
||||
local tsort = {}
|
||||
tsort.__index = tsort
|
||||
function tsort.new()
|
||||
return setmetatable({ n = {} }, tsort)
|
||||
end
|
||||
function tsort:add(...)
|
||||
local p = { ... }
|
||||
local c = #p
|
||||
if c == 0 then return self end
|
||||
if c == 1 then
|
||||
p = p[1]
|
||||
if type(p) == "table" then
|
||||
c = #p
|
||||
else
|
||||
p = { p }
|
||||
end
|
||||
end
|
||||
local n = self.n
|
||||
for i=1, c do
|
||||
local f = p[i]
|
||||
if n[f] == nil then n[f] = {} end
|
||||
end
|
||||
for i=2, c, 1 do
|
||||
local f = p[i]
|
||||
local t = p[i-1]
|
||||
local o = n[f]
|
||||
o[#o+1] = t
|
||||
end
|
||||
return self
|
||||
end
|
||||
function tsort:sort()
|
||||
local n = self.n
|
||||
local s = {}
|
||||
local m = {}
|
||||
for k in pairs(n) do
|
||||
if m[k] == nil then
|
||||
if visit(k, n, m, s) then
|
||||
return nil, "There is a circular dependency in the graph. It is not possible to derive a topological sort."
|
||||
end
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
return tsort
|
|
@ -1,737 +0,0 @@
|
|||
--
|
||||
-- lovebird
|
||||
--
|
||||
-- Copyright (c) 2017 rxi
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
local socket = require "socket"
|
||||
|
||||
local lovebird = { _version = "0.4.3" }
|
||||
|
||||
lovebird.loadstring = loadstring or load
|
||||
lovebird.inited = false
|
||||
lovebird.host = "*"
|
||||
lovebird.buffer = ""
|
||||
lovebird.lines = {}
|
||||
lovebird.connections = {}
|
||||
lovebird.pages = {}
|
||||
|
||||
lovebird.wrapprint = true
|
||||
lovebird.timestamp = true
|
||||
lovebird.allowhtml = false
|
||||
lovebird.echoinput = true
|
||||
lovebird.port = 8000
|
||||
lovebird.whitelist = { "127.0.0.1" }
|
||||
lovebird.maxlines = 200
|
||||
lovebird.updateinterval = .5
|
||||
|
||||
|
||||
lovebird.pages["index"] = [[
|
||||
<?lua
|
||||
-- Handle console input
|
||||
if req.parsedbody.input then
|
||||
local str = req.parsedbody.input
|
||||
if lovebird.echoinput then
|
||||
lovebird.pushline({ type = 'input', str = str })
|
||||
end
|
||||
if str:find("^=") then
|
||||
str = "print(" .. str:sub(2) .. ")"
|
||||
end
|
||||
xpcall(function() assert(lovebird.loadstring(str, "input"))() end,
|
||||
lovebird.onerror)
|
||||
end
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="x-ua-compatible" content="IE=Edge"/>
|
||||
<meta charset="utf-8">
|
||||
<title>lovebird</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
font-family: helvetica, verdana, sans;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
form {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.timestamp {
|
||||
color: #909090;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.repeatcount {
|
||||
color: #F0F0F0;
|
||||
background: #505050;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
border-radius: 7px;
|
||||
display: inline-block;
|
||||
}
|
||||
.errormarker {
|
||||
color: #F0F0F0;
|
||||
background: #8E0000;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
width: 17px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
display: inline-block;
|
||||
}
|
||||
.greybordered {
|
||||
margin: 12px;
|
||||
background: #F0F0F0;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.inputline {
|
||||
font-family: mono, courier;
|
||||
font-size: 13px;
|
||||
color: #606060;
|
||||
}
|
||||
.inputline:before {
|
||||
content: '\00B7\00B7\00B7';
|
||||
padding-right: 5px;
|
||||
}
|
||||
.errorline {
|
||||
color: #8E0000;
|
||||
}
|
||||
#header {
|
||||
background: #101010;
|
||||
height: 25px;
|
||||
color: #F0F0F0;
|
||||
padding: 9px
|
||||
}
|
||||
#title {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
}
|
||||
#title a {
|
||||
color: #F0F0F0;
|
||||
text-decoration: none;
|
||||
}
|
||||
#title a:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#version {
|
||||
font-size: 10px;
|
||||
}
|
||||
#status {
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
#main a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
background: #E0E0E0;
|
||||
border: 1px solid #D0D0D0;
|
||||
border-radius: 3px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
#main a:hover {
|
||||
background: #D0D0D0;
|
||||
border: 1px solid #C0C0C0;
|
||||
}
|
||||
#console {
|
||||
position: absolute;
|
||||
top: 40px; bottom: 0px; left: 0px; right: 312px;
|
||||
}
|
||||
#input {
|
||||
position: absolute;
|
||||
margin: 10px;
|
||||
bottom: 0px; left: 0px; right: 0px;
|
||||
}
|
||||
#inputbox {
|
||||
width: 100%;
|
||||
font-family: mono, courier;
|
||||
font-size: 13px;
|
||||
}
|
||||
#output {
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
margin: 10px;
|
||||
line-height: 17px;
|
||||
top: 0px; bottom: 36px; left: 0px; right: 0px;
|
||||
}
|
||||
#env {
|
||||
position: absolute;
|
||||
top: 40px; bottom: 0px; right: 0px;
|
||||
width: 300px;
|
||||
}
|
||||
#envheader {
|
||||
padding: 5px;
|
||||
background: #E0E0E0;
|
||||
}
|
||||
#envvars {
|
||||
position: absolute;
|
||||
left: 0px; right: 0px; top: 25px; bottom: 0px;
|
||||
margin: 10px;
|
||||
overflow-y: scroll;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<div id="title">
|
||||
<a href="https://github.com/rxi/lovebird">lovebird</a>
|
||||
<span id="version"><?lua echo(lovebird._version) ?></span>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="console" class="greybordered">
|
||||
<div id="output"> <?lua echo(lovebird.buffer) ?> </div>
|
||||
<div id="input">
|
||||
<form method="post"
|
||||
onkeydown="return onInputKeyDown(event);"
|
||||
onsubmit="onInputSubmit(); return false;">
|
||||
<input id="inputbox" name="input" type="text"
|
||||
autocomplete="off"></input>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="env" class="greybordered">
|
||||
<div id="envheader"></div>
|
||||
<div id="envvars"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("inputbox").focus();
|
||||
|
||||
var changeFavicon = function(href) {
|
||||
var old = document.getElementById("favicon");
|
||||
if (old) document.head.removeChild(old);
|
||||
var link = document.createElement("link");
|
||||
link.id = "favicon";
|
||||
link.rel = "shortcut icon";
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
var truncate = function(str, len) {
|
||||
if (str.length <= len) return str;
|
||||
return str.substring(0, len - 3) + "...";
|
||||
}
|
||||
|
||||
var geturl = function(url, onComplete, onFail) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status == 200) {
|
||||
if (onComplete) onComplete(req.responseText);
|
||||
} else {
|
||||
if (onFail) onFail(req.responseText);
|
||||
}
|
||||
}
|
||||
url += (url.indexOf("?") > -1 ? "&_=" : "?_=") + Math.random();
|
||||
req.open("GET", url, true);
|
||||
req.send();
|
||||
}
|
||||
|
||||
var divContentCache = {}
|
||||
var updateDivContent = function(id, content) {
|
||||
if (divContentCache[id] != content) {
|
||||
document.getElementById(id).innerHTML = content;
|
||||
divContentCache[id] = content
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var onInputSubmit = function() {
|
||||
var b = document.getElementById("inputbox");
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/", true);
|
||||
req.send("input=" + encodeURIComponent(b.value));
|
||||
/* Do input history */
|
||||
if (b.value && inputHistory[0] != b.value) {
|
||||
inputHistory.unshift(b.value);
|
||||
}
|
||||
inputHistory.index = -1;
|
||||
/* Reset */
|
||||
b.value = "";
|
||||
refreshOutput();
|
||||
}
|
||||
|
||||
/* Input box history */
|
||||
var inputHistory = [];
|
||||
inputHistory.index = 0;
|
||||
var onInputKeyDown = function(e) {
|
||||
var key = e.which || e.keyCode;
|
||||
if (key != 38 && key != 40) return true;
|
||||
var b = document.getElementById("inputbox");
|
||||
if (key == 38 && inputHistory.index < inputHistory.length - 1) {
|
||||
/* Up key */
|
||||
inputHistory.index++;
|
||||
}
|
||||
if (key == 40 && inputHistory.index >= 0) {
|
||||
/* Down key */
|
||||
inputHistory.index--;
|
||||
}
|
||||
b.value = inputHistory[inputHistory.index] || "";
|
||||
b.selectionStart = b.value.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Output buffer and status */
|
||||
var refreshOutput = function() {
|
||||
geturl("/buffer", function(text) {
|
||||
updateDivContent("status", "connected ●");
|
||||
if (updateDivContent("output", text)) {
|
||||
var div = document.getElementById("output");
|
||||
div.scrollTop = div.scrollHeight;
|
||||
}
|
||||
/* Update favicon */
|
||||
changeFavicon("data:image/png;base64," +
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAP1BMVEUAAAAAAAAAAAD////19fUO"+
|
||||
"Dg7v7+/h4eGzs7MlJSUeHh7n5+fY2NjJycnGxsa3t7eioqKfn5+QkJCHh4d+fn7zU+b5AAAAAnRS"+
|
||||
"TlPlAFWaypEAAABRSURBVBjTfc9HDoAwDERRQ+w0ern/WQkZaUBC4e/mrWzppH9VJjbjZg1Ii2rM"+
|
||||
"DyR1JZ8J0dVWggIGggcEwgbYCRbuPRqgyjHNpzUP+39GPu9fgloC5L9DO0sAAAAASUVORK5CYII="
|
||||
);
|
||||
},
|
||||
function(text) {
|
||||
updateDivContent("status", "disconnected ○");
|
||||
/* Update favicon */
|
||||
changeFavicon("data:image/png;base64," +
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAADZ2dm4uLgM"+
|
||||
"DAz29vbz8/Pv7+/h4eHIyMiwsLBtbW0lJSUeHh4QEBDn5+fS0tLDw8O0tLSioqKfn5+QkJCHh4d+"+
|
||||
"fn5ycnJmZmZgYGBXV1dLS0tFRUUGBgZ0He44AAAAAnRSTlPlAFWaypEAAABeSURBVBjTfY9HDoAw"+
|
||||
"DAQD6Z3ey/9/iXMxkVDYw0g7F3tJReosUKHnwY4pCM+EtOEVXrb7wVRA0dMbaAcUwiVeDQq1Jp4a"+
|
||||
"xUg5kE0ooqZu68Di2Tgbs/DiY/9jyGf+AyFKBAK7KD2TAAAAAElFTkSuQmCC"
|
||||
);
|
||||
});
|
||||
}
|
||||
setInterval(refreshOutput,
|
||||
<?lua echo(lovebird.updateinterval) ?> * 1000);
|
||||
|
||||
/* Environment variable view */
|
||||
var envPath = "";
|
||||
var refreshEnv = function() {
|
||||
geturl("/env.json?p=" + envPath, function(text) {
|
||||
var json = eval("(" + text + ")");
|
||||
|
||||
/* Header */
|
||||
var html = "<a href='#' onclick=\"setEnvPath('')\">env</a>";
|
||||
var acc = "";
|
||||
var p = json.path != "" ? json.path.split(".") : [];
|
||||
for (var i = 0; i < p.length; i++) {
|
||||
acc += "." + p[i];
|
||||
html += " <a href='#' onclick=\"setEnvPath('" + acc + "')\">" +
|
||||
truncate(p[i], 10) + "</a>";
|
||||
}
|
||||
updateDivContent("envheader", html);
|
||||
|
||||
/* Handle invalid table path */
|
||||
if (!json.valid) {
|
||||
updateDivContent("envvars", "Bad path");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Variables */
|
||||
var html = "<table>";
|
||||
for (var i = 0; json.vars[i]; i++) {
|
||||
var x = json.vars[i];
|
||||
var fullpath = (json.path + "." + x.key).replace(/^\./, "");
|
||||
var k = truncate(x.key, 15);
|
||||
if (x.type == "table") {
|
||||
k = "<a href='#' onclick=\"setEnvPath('" + fullpath + "')\">" +
|
||||
k + "</a>";
|
||||
}
|
||||
var v = "<a href='#' onclick=\"insertVar('" +
|
||||
fullpath.replace(/\.(-?[0-9]+)/g, "[$1]") +
|
||||
"');\">" + x.value + "</a>"
|
||||
html += "<tr><td>" + k + "</td><td>" + v + "</td></tr>";
|
||||
}
|
||||
html += "</table>";
|
||||
updateDivContent("envvars", html);
|
||||
});
|
||||
}
|
||||
var setEnvPath = function(p) {
|
||||
envPath = p;
|
||||
refreshEnv();
|
||||
}
|
||||
var insertVar = function(p) {
|
||||
var b = document.getElementById("inputbox");
|
||||
b.value += p;
|
||||
b.focus();
|
||||
}
|
||||
setInterval(refreshEnv, <?lua echo(lovebird.updateinterval) ?> * 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
]]
|
||||
|
||||
|
||||
lovebird.pages["buffer"] = [[ <?lua echo(lovebird.buffer) ?> ]]
|
||||
|
||||
|
||||
lovebird.pages["env.json"] = [[
|
||||
<?lua
|
||||
local t = _G
|
||||
local p = req.parsedurl.query.p or ""
|
||||
p = p:gsub("%.+", "."):match("^[%.]*(.*)[%.]*$")
|
||||
if p ~= "" then
|
||||
for x in p:gmatch("[^%.]+") do
|
||||
t = t[x] or t[tonumber(x)]
|
||||
-- Return early if path does not exist
|
||||
if type(t) ~= "table" then
|
||||
echo('{ "valid": false, "path": ' .. string.format("%q", p) .. ' }')
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
?>
|
||||
{
|
||||
"valid": true,
|
||||
"path": "<?lua echo(p) ?>",
|
||||
"vars": [
|
||||
<?lua
|
||||
local keys = {}
|
||||
for k in pairs(t) do
|
||||
if type(k) == "number" or type(k) == "string" then
|
||||
table.insert(keys, k)
|
||||
end
|
||||
end
|
||||
table.sort(keys, lovebird.compare)
|
||||
for _, k in pairs(keys) do
|
||||
local v = t[k]
|
||||
?>
|
||||
{
|
||||
"key": "<?lua echo(k) ?>",
|
||||
"value": <?lua echo(
|
||||
string.format("%q",
|
||||
lovebird.truncate(
|
||||
lovebird.htmlescape(
|
||||
tostring(v)), 26))) ?>,
|
||||
"type": "<?lua echo(type(v)) ?>",
|
||||
},
|
||||
<?lua end ?>
|
||||
]
|
||||
}
|
||||
]]
|
||||
|
||||
|
||||
|
||||
function lovebird.init()
|
||||
-- Init server
|
||||
lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
|
||||
lovebird.addr, lovebird.port = lovebird.server:getsockname()
|
||||
lovebird.server:settimeout(0)
|
||||
-- Wrap print
|
||||
lovebird.origprint = print
|
||||
if lovebird.wrapprint then
|
||||
local oldprint = print
|
||||
print = function(...)
|
||||
oldprint(...)
|
||||
lovebird.print(...)
|
||||
end
|
||||
end
|
||||
-- Compile page templates
|
||||
for k, page in pairs(lovebird.pages) do
|
||||
lovebird.pages[k] = lovebird.template(page, "lovebird, req",
|
||||
"pages." .. k)
|
||||
end
|
||||
lovebird.inited = true
|
||||
end
|
||||
|
||||
|
||||
function lovebird.template(str, params, chunkname)
|
||||
params = params and ("," .. params) or ""
|
||||
local f = function(x) return string.format(" echo(%q)", x) end
|
||||
str = ("?>"..str.."<?lua"):gsub("%?>(.-)<%?lua", f)
|
||||
str = "local echo " .. params .. " = ..." .. str
|
||||
local fn = assert(lovebird.loadstring(str, chunkname))
|
||||
return function(...)
|
||||
local output = {}
|
||||
local echo = function(str) table.insert(output, str) end
|
||||
fn(echo, ...)
|
||||
return table.concat(lovebird.map(output, tostring))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.map(t, fn)
|
||||
local res = {}
|
||||
for k, v in pairs(t) do res[k] = fn(v) end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
function lovebird.trace(...)
|
||||
local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
|
||||
print(str)
|
||||
if not lovebird.wrapprint then lovebird.print(str) end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.unescape(str)
|
||||
local f = function(x) return string.char(tonumber("0x"..x)) end
|
||||
return (str:gsub("%+", " "):gsub("%%(..)", f))
|
||||
end
|
||||
|
||||
|
||||
function lovebird.parseurl(url)
|
||||
local res = {}
|
||||
res.path, res.search = url:match("/([^%?]*)%??(.*)")
|
||||
res.query = {}
|
||||
for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
|
||||
res.query[k] = lovebird.unescape(v)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
local htmlescapemap = {
|
||||
["<"] = "<",
|
||||
["&"] = "&",
|
||||
['"'] = """,
|
||||
["'"] = "'",
|
||||
}
|
||||
|
||||
function lovebird.htmlescape(str)
|
||||
return ( str:gsub("[<&\"']", htmlescapemap) )
|
||||
end
|
||||
|
||||
|
||||
function lovebird.truncate(str, len)
|
||||
if #str <= len then
|
||||
return str
|
||||
end
|
||||
return str:sub(1, len - 3) .. "..."
|
||||
end
|
||||
|
||||
|
||||
function lovebird.compare(a, b)
|
||||
local na, nb = tonumber(a), tonumber(b)
|
||||
if na then
|
||||
if nb then return na < nb end
|
||||
return false
|
||||
elseif nb then
|
||||
return true
|
||||
end
|
||||
return tostring(a) < tostring(b)
|
||||
end
|
||||
|
||||
|
||||
function lovebird.checkwhitelist(addr)
|
||||
if lovebird.whitelist == nil then return true end
|
||||
for _, a in pairs(lovebird.whitelist) do
|
||||
local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
|
||||
if addr:match(ptn) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function lovebird.clear()
|
||||
lovebird.lines = {}
|
||||
lovebird.buffer = ""
|
||||
end
|
||||
|
||||
|
||||
function lovebird.pushline(line)
|
||||
line.time = os.time()
|
||||
line.count = 1
|
||||
table.insert(lovebird.lines, line)
|
||||
if #lovebird.lines > lovebird.maxlines then
|
||||
table.remove(lovebird.lines, 1)
|
||||
end
|
||||
lovebird.recalcbuffer()
|
||||
end
|
||||
|
||||
|
||||
function lovebird.recalcbuffer()
|
||||
local function doline(line)
|
||||
local str = line.str
|
||||
if not lovebird.allowhtml then
|
||||
str = lovebird.htmlescape(line.str):gsub("\n", "<br>")
|
||||
end
|
||||
if line.type == "input" then
|
||||
str = '<span class="inputline">' .. str .. '</span>'
|
||||
else
|
||||
if line.type == "error" then
|
||||
str = '<span class="errormarker">!</span> ' .. str
|
||||
str = '<span class="errorline">' .. str .. '</span>'
|
||||
end
|
||||
if line.count > 1 then
|
||||
str = '<span class="repeatcount">' .. line.count .. '</span> ' .. str
|
||||
end
|
||||
if lovebird.timestamp then
|
||||
str = os.date('<span class="timestamp">%H:%M:%S</span> ', line.time) ..
|
||||
str
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "<br>")
|
||||
end
|
||||
|
||||
|
||||
function lovebird.print(...)
|
||||
local t = {}
|
||||
for i = 1, select("#", ...) do
|
||||
table.insert(t, tostring(select(i, ...)))
|
||||
end
|
||||
local str = table.concat(t, " ")
|
||||
local last = lovebird.lines[#lovebird.lines]
|
||||
if last and str == last.str then
|
||||
-- Update last line if this line is a duplicate of it
|
||||
last.time = os.time()
|
||||
last.count = last.count + 1
|
||||
lovebird.recalcbuffer()
|
||||
else
|
||||
-- Create new line
|
||||
lovebird.pushline({ type = "output", str = str })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.onerror(err)
|
||||
lovebird.pushline({ type = "error", str = err })
|
||||
if lovebird.wrapprint then
|
||||
lovebird.origprint("[lovebird] ERROR: " .. err)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.onrequest(req, client)
|
||||
local page = req.parsedurl.path
|
||||
page = page ~= "" and page or "index"
|
||||
-- Handle "page not found"
|
||||
if not lovebird.pages[page] then
|
||||
return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
|
||||
end
|
||||
-- Handle page
|
||||
local str
|
||||
xpcall(function()
|
||||
local data = lovebird.pages[page](lovebird, req)
|
||||
local contenttype = "text/html"
|
||||
if string.match(page, "%.json$") then
|
||||
contenttype = "application/json"
|
||||
end
|
||||
str = "HTTP/1.1 200 OK\r\n" ..
|
||||
"Content-Type: " .. contenttype .. "\r\n" ..
|
||||
"Content-Length: " .. #data .. "\r\n" ..
|
||||
"\r\n" .. data
|
||||
end, lovebird.onerror)
|
||||
return str
|
||||
end
|
||||
|
||||
|
||||
function lovebird.receive(client, pattern)
|
||||
while 1 do
|
||||
local data, msg = client:receive(pattern)
|
||||
if not data then
|
||||
if msg == "timeout" then
|
||||
-- Wait for more data
|
||||
coroutine.yield(true)
|
||||
else
|
||||
-- Disconnected -- yielding nil means we're done
|
||||
coroutine.yield(nil)
|
||||
end
|
||||
else
|
||||
return data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.send(client, data)
|
||||
local idx = 1
|
||||
while idx < #data do
|
||||
local res, msg = client:send(data, idx)
|
||||
if not res and msg == "closed" then
|
||||
-- Handle disconnect
|
||||
coroutine.yield(nil)
|
||||
else
|
||||
idx = idx + res
|
||||
coroutine.yield(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.onconnect(client)
|
||||
-- Create request table
|
||||
local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
|
||||
local req = {}
|
||||
req.socket = client
|
||||
req.addr, req.port = client:getsockname()
|
||||
req.request = lovebird.receive(client, "*l")
|
||||
req.method, req.url, req.proto = req.request:match(requestptn)
|
||||
req.headers = {}
|
||||
while 1 do
|
||||
local line, msg = lovebird.receive(client, "*l")
|
||||
if not line or #line == 0 then break end
|
||||
local k, v = line:match("(.-):%s*(.*)$")
|
||||
req.headers[k] = v
|
||||
end
|
||||
if req.headers["Content-Length"] then
|
||||
req.body = lovebird.receive(client, req.headers["Content-Length"])
|
||||
end
|
||||
-- Parse body
|
||||
req.parsedbody = {}
|
||||
if req.body then
|
||||
for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
|
||||
req.parsedbody[k] = lovebird.unescape(v)
|
||||
end
|
||||
end
|
||||
-- Parse request line's url
|
||||
req.parsedurl = lovebird.parseurl(req.url)
|
||||
-- Handle request; get data to send and send
|
||||
local data = lovebird.onrequest(req)
|
||||
lovebird.send(client, data)
|
||||
-- Clear up
|
||||
client:close()
|
||||
end
|
||||
|
||||
|
||||
function lovebird.update()
|
||||
if not lovebird.inited then lovebird.init() end
|
||||
-- Handle new connections
|
||||
while 1 do
|
||||
-- Accept new connections
|
||||
local client = lovebird.server:accept()
|
||||
if not client then break end
|
||||
client:settimeout(0)
|
||||
local addr = client:getsockname()
|
||||
if lovebird.checkwhitelist(addr) then
|
||||
-- Connection okay -- create and add coroutine to set
|
||||
local conn = coroutine.wrap(function()
|
||||
xpcall(function() lovebird.onconnect(client) end, function() end)
|
||||
end)
|
||||
lovebird.connections[conn] = true
|
||||
else
|
||||
-- Reject connection not on whitelist
|
||||
lovebird.trace("got non-whitelisted connection attempt: ", addr)
|
||||
client:close()
|
||||
end
|
||||
end
|
||||
-- Handle existing connections
|
||||
for conn in pairs(lovebird.connections) do
|
||||
-- Resume coroutine, remove if it has finished
|
||||
local status = conn()
|
||||
if status == nil then
|
||||
lovebird.connections[conn] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return lovebird
|
|
@ -1,16 +0,0 @@
|
|||
local Filesystem = {}
|
||||
|
||||
function Filesystem.exists(filepath)
|
||||
local info = love.filesystem.getInfo( filepath )
|
||||
local exists = false
|
||||
|
||||
if (info == nil) then
|
||||
exists = false
|
||||
else
|
||||
exists = true
|
||||
end
|
||||
|
||||
return exists
|
||||
end
|
||||
|
||||
return Filesystem
|
|
@ -1,64 +0,0 @@
|
|||
local Graphics = {}
|
||||
|
||||
function Graphics.resetColor()
|
||||
love.graphics.setColor(1,1,1,1)
|
||||
end
|
||||
|
||||
function Graphics.box(x, y, w, h)
|
||||
local x = math.floor(x)
|
||||
local y = math.floor(y)
|
||||
local w = math.floor(w)
|
||||
local h = math.floor(h)
|
||||
local a = a or 1
|
||||
|
||||
local r, g, b, a = love.graphics.getColor( )
|
||||
|
||||
love.graphics.setColor(r, g, b, 0.3 * a)
|
||||
love.graphics.rectangle("fill", x, y, w, h)
|
||||
|
||||
love.graphics.setColor(r, g, b, a)
|
||||
love.graphics.rectangle("line", x, y, w, h)
|
||||
end
|
||||
|
||||
function Graphics.print(text, x, y, align, r, sx, sy, ox, oy, kx, ky)
|
||||
local width
|
||||
local font = love.graphics.getFont()
|
||||
width = font:getWidth(text)
|
||||
|
||||
if align == "center" then
|
||||
width = (width/2)
|
||||
elseif align == "right" then
|
||||
width = width
|
||||
else
|
||||
width = 0
|
||||
end
|
||||
|
||||
love.graphics.print(text, x - (width), y, r, sx, sy, ox, oy, kx, ky)
|
||||
end
|
||||
|
||||
function Graphics.printWithSpacing(text, spacing, align, x, y, r, sx, sy, ox, oy, kx, ky)
|
||||
-- DO NOT USE THIS FUNCTION IN A "UPDATE" FUNCTION !
|
||||
-- it's pretty heavy to use as it use a loop to get every character in a text
|
||||
local font = love.graphics.getFont()
|
||||
local xx = 0
|
||||
local lenght = string.len(text)
|
||||
local basewidth = font:getWidth(text)
|
||||
local width = basewidth + (spacing * lenght)
|
||||
|
||||
if align == "center" then
|
||||
width = (width/2)
|
||||
elseif align == "right" then
|
||||
width = width
|
||||
else
|
||||
width = 0
|
||||
end
|
||||
|
||||
for i=1, lenght do
|
||||
local char = string.sub(text, i, i)
|
||||
pos = math.floor(x + xx - width)
|
||||
love.graphics.print(char, pos, y)
|
||||
xx = xx + font:getWidth(char) + spacing
|
||||
end
|
||||
end
|
||||
|
||||
return Graphics
|
|
@ -1,7 +0,0 @@
|
|||
local cwd = (...):gsub('%.init$', '') .. "."
|
||||
|
||||
return {
|
||||
math = require(cwd .. "math"),
|
||||
graphics = require(cwd .. "graphics"),
|
||||
filesystem = require(cwd .. "filesystem")
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
local Math = {}
|
||||
|
||||
function Math.wrap(x, startnumber, endnumber)
|
||||
local delta = endnumber - startnumber
|
||||
if delta < 0 then
|
||||
err("endnumber must be larger than startnumber")
|
||||
end
|
||||
while x >= endnumber do
|
||||
x = x - delta
|
||||
end
|
||||
|
||||
while x < startnumber do
|
||||
x = x + delta
|
||||
end
|
||||
|
||||
return x
|
||||
end
|
||||
|
||||
function Math.sign(x)
|
||||
if (x < 0) then
|
||||
return -1
|
||||
elseif (x > 0) then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
function Math.round(num)
|
||||
return math.floor(num + 0.5)
|
||||
end
|
||||
|
||||
function Math.vector(x1, y1, x2, y2)
|
||||
local vecx, vecy
|
||||
|
||||
vecx = x2 - x1
|
||||
vexy = y2 - y1
|
||||
|
||||
return vecx, vecy
|
||||
end
|
||||
|
||||
function Math.getMiddlePoint(x1, y1, x2, y2)
|
||||
local newx, newy, vecx, vecy
|
||||
|
||||
vecx = math.max(x1, x2) - math.min(x1, x2)
|
||||
vecy = math.max(y1, y2) - math.min(y1, y2)
|
||||
|
||||
newx = math.min(x1, x2) + (vecx / 2)
|
||||
newy = math.min(y1, y2) + (vecy / 2)
|
||||
|
||||
return newx, newy
|
||||
end
|
||||
|
||||
function Math.pointDistance(x1, y1, x2, y2)
|
||||
local vecx, vecy
|
||||
|
||||
vecx = math.max(x1, x2) - math.min(x1, x2)
|
||||
vecy = math.max(y1, y2) - math.min(y1, y2)
|
||||
|
||||
return math.sqrt(vecx^2 + vecy^2)
|
||||
|
||||
end
|
||||
|
||||
function Math.pointDirection(x1,y1,x2,y2)
|
||||
local vecx, vecy, angle
|
||||
vecy = y2 - y1
|
||||
vecx = x2 - x1
|
||||
angle = math.atan2(vecy, vecx)
|
||||
|
||||
return angle
|
||||
end
|
||||
|
||||
function Math.numberToString(x, length)
|
||||
local length = length or 1
|
||||
local string = ""
|
||||
local x = x
|
||||
if (x >= math.pow(10, length)) then
|
||||
x = unitsNumber*10 - 1
|
||||
string = string .. x
|
||||
else
|
||||
for i=1, (length-1) do
|
||||
if (x < math.pow(10, length-i)) then
|
||||
string = string .. "0"
|
||||
end
|
||||
end
|
||||
string = string .. x
|
||||
end
|
||||
return string
|
||||
end
|
||||
|
||||
function Math.floorCoord(x, y)
|
||||
return math.floor(x), math.floor(y)
|
||||
end
|
||||
|
||||
function Math.pixeliseCoord(x, y, factor)
|
||||
x, y = Math.floorCoord(x / factor, y / factor)
|
||||
|
||||
x = x * factor
|
||||
y = y * factor
|
||||
|
||||
return x, y
|
||||
end
|
||||
|
||||
return Math
|
Loading…
Reference in a new issue