WIP: Auios' version - Do not merge. Only here for comparison #9

Draft
Auios wants to merge 28 commits from Auios/BlockLua:master into master
16 changed files with 2824 additions and 2596 deletions
Showing only changes of commit 01f216f31e - Show all commits

2
.gitignore vendored
View File

@@ -1,3 +1 @@
.*
!.gitignore
build/ build/

View File

@@ -46,10 +46,12 @@ function filter.chain(...)
while true do while true do
if index == top then if index == top then
chunk = arg[index](chunk) chunk = arg[index](chunk)
if chunk == "" or top == n then return chunk if chunk == "" or top == n then
elseif chunk then index = index + 1 return chunk
elseif chunk then
index = index + 1
else else
top = top+1 top = top + 1
index = top index = top
end end
else else
@@ -58,9 +60,14 @@ function filter.chain(...)
index = index - 1 index = index - 1
chunk = retry chunk = retry
elseif chunk then elseif chunk then
if index == n then return chunk if index == n then
else index = index + 1 end return chunk
else base.error("filter returned inappropriate nil") end else
index = index + 1
end
else
base.error("filter returned inappropriate nil")
end
end end
end end
end end
@@ -93,7 +100,9 @@ function source.file(handle, io_err)
if not chunk then handle:close() end if not chunk then handle:close() end
return chunk return chunk
end end
else return source.error(io_err or "unable to open file") end else
return source.error(io_err or "unable to open file")
end
end end
-- turns a fancy source into a simple source -- turns a fancy source into a simple source
@@ -102,8 +111,11 @@ function source.simplify(src)
return function() return function()
local chunk, err_or_new = src() local chunk, err_or_new = src()
src = err_or_new or src src = err_or_new or src
if not chunk then return nil, err_or_new if not chunk then
else return chunk end return nil, err_or_new
else
return chunk
end
end end
end end
@@ -112,12 +124,17 @@ function source.string(s)
if s then if s then
local i = 1 local i = 1
return function() return function()
local chunk = string.sub(s, i, i+BLOCKSIZE-1) local chunk = string.sub(s, i, i + BLOCKSIZE - 1)
i = i + BLOCKSIZE i = i + BLOCKSIZE
if chunk ~= "" then return chunk if chunk ~= "" then
else return nil end return chunk
else
return nil
end
end
else
return source.empty()
end end
else return source.empty() end
end end
-- creates rewindable source -- creates rewindable source
@@ -127,8 +144,11 @@ function source.rewind(src)
return function(chunk) return function(chunk)
if not chunk then if not chunk then
chunk = table.remove(t) chunk = table.remove(t)
if not chunk then return src() if not chunk then
else return chunk end return src()
else
return chunk
end
else else
table.insert(t, chunk) table.insert(t, chunk)
end end
@@ -228,9 +248,13 @@ function sink.file(handle, io_err)
if not chunk then if not chunk then
handle:close() handle:close()
return 1 return 1
else return handle:write(chunk) end else
return handle:write(chunk)
end
end
else
return sink.error(io_err or "unable to open file")
end end
else return sink.error(io_err or "unable to open file") end
end end
-- creates a sink that discards data -- creates a sink that discards data
@@ -262,7 +286,9 @@ function sink.chain(f, snk)
if filtered == done then return 1 end if filtered == done then return 1 end
filtered = f(done) filtered = f(done)
end end
else return 1 end else
return 1
end
end end
end end
@@ -273,8 +299,11 @@ end
function pump.step(src, snk) function pump.step(src, snk)
local chunk, src_err = src() local chunk, src_err = src()
local ret, snk_err = snk(chunk, src_err) local ret, snk_err = snk(chunk, src_err)
if chunk and ret then return 1 if chunk and ret then
else return nil, src_err or snk_err end return 1
else
return nil, src_err or snk_err
end
end end
-- pumps all data from a source to a sink, using a step function -- pumps all data from a source to a sink, using a step function
@@ -284,9 +313,11 @@ function pump.all(src, snk, step)
while true do while true do
local ret, err = step(src, snk) local ret, err = step(src, snk)
if not ret then if not ret then
if err then return nil, err if err then
else return 1 end return nil, err
else
return 1
end
end end
end end
end end

View File

@@ -29,7 +29,9 @@ local function choose(table)
local f = table[name or "nil"] local f = table[name or "nil"]
if not f then if not f then
base.error("unknown key (" .. base.tostring(name) .. ")", 3) base.error("unknown key (" .. base.tostring(name) .. ")", 3)
else return f(opt1, opt2) end else
return f(opt1, opt2)
end
end end
end end
@@ -54,9 +56,14 @@ end
local function format(chunk) local function format(chunk)
if chunk then if chunk then
if chunk == "" then return "''" if chunk == "" then
else return string.len(chunk) end return "''"
else return "nil" end else
return string.len(chunk)
end
else
return "nil"
end
end end
-- define the line-wrap filters -- define the line-wrap filters

View File

@@ -47,8 +47,11 @@ function choose(table)
name, opt1, opt2 = "default", name, opt1 name, opt1, opt2 = "default", name, opt1
end end
local f = table[name or "nil"] local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) if not f then
else return f(opt1, opt2) end base.error("unknown key (" .. base.tostring(name) .. ")", 3)
else
return f(opt1, opt2)
end
end end
end end
@@ -70,7 +73,9 @@ sinkt["close-when-done"] = function(sock)
if not chunk then if not chunk then
sock:close() sock:close()
return 1 return 1
else return sock:send(chunk) end else
return sock:send(chunk)
end
end end
}) })
end end
@@ -81,8 +86,11 @@ sinkt["keep-open"] = function(sock)
dirty = function() return sock:dirty() end dirty = function() return sock:dirty() end
}, { }, {
__call = function(self, chunk, err) __call = function(self, chunk, err)
if chunk then return sock:send(chunk) if chunk then
else return 1 end return sock:send(chunk)
else
return 1
end
end end
}) })
end end
@@ -116,12 +124,15 @@ sourcet["until-closed"] = function(sock)
__call = function() __call = function()
if done then return nil end if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE) local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk if not err then
return chunk
elseif err == "closed" then elseif err == "closed" then
sock:close() sock:close()
done = 1 done = 1
return partial return partial
else return nil, err end else
return nil, err
end
end end
}) })
end end
@@ -130,4 +141,3 @@ end
sourcet["default"] = sourcet["until-closed"] sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet) source = choose(sourcet)

View File

@@ -57,7 +57,7 @@ end
function metat.__index:login(user, password) function metat.__index:login(user, password)
self.try(self.tp:command("user", user or USER)) self.try(self.tp:command("user", user or USER))
local code, reply = self.try(self.tp:check{"2..", 331}) local code, reply = self.try(self.tp:check { "2..", 331 })
if code == 331 then if code == 331 then
self.try(self.tp:command("pass", password or PASSWORD)) self.try(self.tp:command("pass", password or PASSWORD))
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
@@ -73,7 +73,7 @@ function metat.__index:pasv()
self.try(a and b and c and d and p1 and p2, reply) self.try(a and b and c and d and p1 and p2, reply)
self.pasvt = { self.pasvt = {
ip = string.format("%d.%d.%d.%d", a, b, c, d), ip = string.format("%d.%d.%d.%d", a, b, c, d),
port = p1*256 + p2 port = p1 * 256 + p2
} }
if self.server then if self.server then
self.server:close() self.server:close()
@@ -91,7 +91,7 @@ function metat.__index:port(ip, port)
self.try(self.server:settimeout(TIMEOUT)) self.try(self.server:settimeout(TIMEOUT))
end end
local pl = math.mod(port, 256) local pl = math.mod(port, 256)
local ph = (port - pl)/256 local ph = (port - pl) / 256
local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",") local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",")
self.try(self.tp:command("port", arg)) self.try(self.tp:command("port", arg))
self.try(self.tp:check("2..")) self.try(self.tp:check("2.."))
@@ -110,13 +110,13 @@ function metat.__index:send(sendt)
local command = sendt.command or "stor" local command = sendt.command or "stor"
-- send the transfer command and check the reply -- send the transfer command and check the reply
self.try(self.tp:command(command, argument)) self.try(self.tp:command(command, argument))
local code, reply = self.try(self.tp:check{"2..", "1.."}) local code, reply = self.try(self.tp:check { "2..", "1.." })
-- if there is not a a pasvt table, then there is a server -- if there is not a a pasvt table, then there is a server
-- and we already sent a PORT command -- and we already sent a PORT command
if not self.pasvt then self:portconnect() end if not self.pasvt then self:portconnect() end
-- get the sink, source and step for the transfer -- get the sink, source and step for the transfer
local step = sendt.step or ltn12.pump.step local step = sendt.step or ltn12.pump.step
local readt = {self.tp.c} local readt = { self.tp.c }
local checkstep = function(src, snk) local checkstep = function(src, snk)
-- check status in control connection while downloading -- check status in control connection while downloading
local readyt = socket.select(readt, nil, 0) local readyt = socket.select(readt, nil, 0)
@@ -143,7 +143,7 @@ function metat.__index:receive(recvt)
if argument == "" then argument = nil end if argument == "" then argument = nil end
local command = recvt.command or "retr" local command = recvt.command or "retr"
self.try(self.tp:command(command, argument)) self.try(self.tp:command(command, argument))
local code = self.try(self.tp:check{"1..", "2.."}) local code = self.try(self.tp:check { "1..", "2.." })
if not self.pasvt then self:portconnect() end if not self.pasvt then self:portconnect() end
local source = socket.source("until-closed", self.data) local source = socket.source("until-closed", self.data)
local step = recvt.step or ltn12.pump.step local step = recvt.step or ltn12.pump.step
@@ -167,7 +167,7 @@ function metat.__index:type(type)
end end
function metat.__index:greet() function metat.__index:greet()
local code = self.try(self.tp:check{"1..", "2.."}) local code = self.try(self.tp:check { "1..", "2.." })
if string.find(code, "1..") then self.try(self.tp:check("2..")) end if string.find(code, "1..") then self.try(self.tp:check("2..")) end
return 1 return 1
end end
@@ -190,11 +190,13 @@ end
local function override(t) local function override(t)
if t.url then if t.url then
local u = url.parse(t.url) local u = url.parse(t.url)
for i,v in base.pairs(t) do for i, v in base.pairs(t) do
u[i] = v u[i] = v
end end
return u return u
else return t end else
return t
end
end end
local function tput(putt) local function tput(putt)
@@ -236,8 +238,11 @@ local function sput(u, body)
end end
put = socket.protect(function(putt, body) put = socket.protect(function(putt, body)
if base.type(putt) == "string" then return sput(putt, body) if base.type(putt) == "string" then
else return tput(putt) end return sput(putt, body)
else
return tput(putt)
end
end) end)
local function tget(gett) local function tget(gett)
@@ -275,7 +280,9 @@ command = socket.protect(function(cmdt)
end) end)
get = socket.protect(function(gett) get = socket.protect(function(gett)
if base.type(gett) == "string" then return sget(gett) if base.type(gett) == "string" then
else return tget(gett) end return sget(gett)
else
return tget(gett)
end
end) end)

View File

@@ -52,8 +52,11 @@ local function receiveheaders(sock, headers)
if err then return nil, err end if err then return nil, err end
end end
-- save pair in table -- save pair in table
if headers[name] then headers[name] = headers[name] .. ", " .. value if headers[name] then
else headers[name] = value end headers[name] = headers[name] .. ", " .. value
else
headers[name] = value
end
end end
return headers return headers
end end
@@ -162,8 +165,11 @@ function metat.__index:receivebody(headers, sink, step)
local length = base.tonumber(headers["content-length"]) local length = base.tonumber(headers["content-length"])
local t = headers["transfer-encoding"] -- shortcut local t = headers["transfer-encoding"] -- shortcut
local mode = "default" -- connection close local mode = "default" -- connection close
if t and t ~= "identity" then mode = "http-chunked" if t and t ~= "identity" then
elseif base.tonumber(headers["content-length"]) then mode = "by-length" end mode = "http-chunked"
elseif base.tonumber(headers["content-length"]) then
mode = "by-length"
end
return self.try(ltn12.pump.all(socket.source(mode, self.c, length), return self.try(ltn12.pump.all(socket.source(mode, self.c, length),
sink, step)) sink, step))
end end
@@ -219,7 +225,7 @@ local function adjustheaders(reqt)
"Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password)) "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password))
end end
-- override with user headers -- override with user headers
for i,v in base.pairs(reqt.headers or lower) do for i, v in base.pairs(reqt.headers or lower) do
lower[string.lower(i)] = v lower[string.lower(i)] = v
end end
return lower return lower
@@ -229,7 +235,7 @@ end
local default = { local default = {
host = "", host = "",
port = PORT, port = PORT,
path ="/", path = "/",
scheme = "http" scheme = "http"
} }
@@ -237,7 +243,7 @@ local function adjustrequest(reqt)
-- parse url if provided -- parse url if provided
local nreqt = reqt.url and url.parse(reqt.url, default) or {} local nreqt = reqt.url and url.parse(reqt.url, default) or {}
-- explicit components override url -- explicit components override url
for i,v in base.pairs(reqt) do nreqt[i] = v end for i, v in base.pairs(reqt) do nreqt[i] = v end
if nreqt.port == "" then nreqt.port = 80 end if nreqt.port == "" then nreqt.port = 80 end
socket.try(nreqt.host and nreqt.host ~= "", socket.try(nreqt.host and nreqt.host ~= "",
"invalid host '" .. base.tostring(nreqt.host) .. "'") "invalid host '" .. base.tostring(nreqt.host) .. "'")
@@ -345,6 +351,9 @@ local function srequest(u, b)
end end
request = socket.protect(function(reqt, body) request = socket.protect(function(reqt, body)
if base.type(reqt) == "string" then return srequest(reqt, body) if base.type(reqt) == "string" then
else return trequest(reqt) end return srequest(reqt, body)
else
return trequest(reqt)
end
end) end)

View File

@@ -102,7 +102,7 @@ end
function metat.__index:send(mailt) function metat.__index:send(mailt)
self:mail(mailt.from) self:mail(mailt.from)
if base.type(mailt.rcpt) == "table" then if base.type(mailt.rcpt) == "table" then
for i,v in base.ipairs(mailt.rcpt) do for i, v in base.ipairs(mailt.rcpt) do
self:rcpt(v) self:rcpt(v)
end end
else else
@@ -114,7 +114,7 @@ end
function open(server, port, create) function open(server, port, create)
local tp = socket.try(tp.connect(server or SERVER, port or PORT, local tp = socket.try(tp.connect(server or SERVER, port or PORT,
TIMEOUT, create)) TIMEOUT, create))
local s = base.setmetatable({tp = tp}, metat) local s = base.setmetatable({ tp = tp }, metat)
-- make sure tp is closed if we get an exception -- make sure tp is closed if we get an exception
s.try = socket.newtry(function() s.try = socket.newtry(function()
s:close() s:close()
@@ -125,7 +125,7 @@ end
-- convert headers to lowercase -- convert headers to lowercase
local function lower_headers(headers) local function lower_headers(headers)
local lower = {} local lower = {}
for i,v in base.pairs(headers or lower) do for i, v in base.pairs(headers or lower) do
lower[string.lower(i)] = v lower[string.lower(i)] = v
end end
return lower return lower
@@ -148,7 +148,7 @@ local send_message
-- yield the headers all at once, it's faster -- yield the headers all at once, it's faster
local function send_headers(headers) local function send_headers(headers)
local h = "\r\n" local h = "\r\n"
for i,v in base.pairs(headers) do for i, v in base.pairs(headers) do
h = i .. ': ' .. v .. "\r\n" .. h h = i .. ': ' .. v .. "\r\n" .. h
end end
coroutine.yield(h) coroutine.yield(h)
@@ -192,9 +192,13 @@ local function send_source(mesgt)
-- send body from source -- send body from source
while true do while true do
local chunk, err = mesgt.body() local chunk, err = mesgt.body()
if err then coroutine.yield(nil, err) if err then
elseif chunk then coroutine.yield(chunk) coroutine.yield(nil, err)
else break end elseif chunk then
coroutine.yield(chunk)
else
break
end
end end
end end
@@ -211,9 +215,13 @@ end
-- message source -- message source
function send_message(mesgt) function send_message(mesgt)
if base.type(mesgt.body) == "table" then send_multipart(mesgt) if base.type(mesgt.body) == "table" then
elseif base.type(mesgt.body) == "function" then send_source(mesgt) send_multipart(mesgt)
else send_string(mesgt) end elseif base.type(mesgt.body) == "function" then
send_source(mesgt)
else
send_string(mesgt)
end
end end
-- set defaul headers -- set defaul headers
@@ -233,8 +241,11 @@ function message(mesgt)
local co = coroutine.create(function() send_message(mesgt) end) local co = coroutine.create(function() send_message(mesgt) end)
return function() return function()
local ret, a, b = coroutine.resume(co) local ret, a, b = coroutine.resume(co)
if ret then return a, b if ret then
else return nil, a end return a, b
else
return nil, a
end
end end
end end

View File

@@ -57,15 +57,20 @@ function metat.__index:check(ok)
end end
return nil, reply return nil, reply
else else
if string.find(code, ok) then return base.tonumber(code), reply if string.find(code, ok) then
else return nil, reply end return base.tonumber(code), reply
else
return nil, reply
end
end
else
return ok(base.tonumber(code), reply)
end end
else return ok(base.tonumber(code), reply) end
end end
function metat.__index:command(cmd, arg) function metat.__index:command(cmd, arg)
if arg then if arg then
return self.c:send(cmd .. " " .. arg.. "\r\n") return self.c:send(cmd .. " " .. arg .. "\r\n")
else else
return self.c:send(cmd .. "\r\n") return self.c:send(cmd .. "\r\n")
end end
@@ -118,6 +123,5 @@ function connect(host, port, timeout, create)
c:close() c:close()
return nil, e return nil, e
end end
return base.setmetatable({c = c}, metat) return base.setmetatable({ c = c }, metat)
end end

View File

@@ -41,7 +41,7 @@ end
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
local function make_set(t) local function make_set(t)
local s = {} local s = {}
for i,v in base.ipairs(t) do for i, v in base.ipairs(t) do
s[t[i]] = 1 s[t[i]] = 1
end end
return s return s
@@ -55,9 +55,12 @@ local segment_set = make_set {
} }
local function protect_segment(s) local function protect_segment(s)
return string.gsub(s, "([^A-Za-z0-9_])", function (c) return string.gsub(s, "([^A-Za-z0-9_])", function(c)
if segment_set[c] then return c if segment_set[c] then
else return string.format("%%%02x", string.byte(c)) end return c
else
return string.format("%%%02x", string.byte(c))
end
end) end)
end end
@@ -86,18 +89,18 @@ local function absolute_path(base_path, relative_path)
if string.sub(relative_path, 1, 1) == "/" then return relative_path end if string.sub(relative_path, 1, 1) == "/" then return relative_path end
local path = string.gsub(base_path, "[^/]*$", "") local path = string.gsub(base_path, "[^/]*$", "")
path = path .. relative_path path = path .. relative_path
path = string.gsub(path, "([^/]*%./)", function (s) path = string.gsub(path, "([^/]*%./)", function(s)
if s ~= "./" then return s else return "" end if s ~= "./" then return s else return "" end
end) end)
path = string.gsub(path, "/%.$", "/") path = string.gsub(path, "/%.$", "/")
local reduced local reduced
while reduced ~= path do while reduced ~= path do
reduced = path reduced = path
path = string.gsub(reduced, "([^/]*/%.%./)", function (s) path = string.gsub(reduced, "([^/]*/%.%./)", function(s)
if s ~= "../../" then return "" else return s end if s ~= "../../" then return "" else return s end
end) end)
end end
path = string.gsub(reduced, "([^/]*/%.%.)$", function (s) path = string.gsub(reduced, "([^/]*/%.%.)$", function(s)
if s ~= "../.." then return "" else return s end if s ~= "../.." then return "" else return s end
end) end)
return path return path
@@ -124,7 +127,7 @@ end
function parse(url, default) function parse(url, default)
-- initialize default parameters -- initialize default parameters
local parsed = {} local parsed = {}
for i,v in base.pairs(default or parsed) do parsed[i] = v end for i, v in base.pairs(default or parsed) do parsed[i] = v end
-- empty url is parsed to nil -- empty url is parsed to nil
if not url or url == "" then return nil, "invalid url" end if not url or url == "" then return nil, "invalid url" end
-- remove whitespace -- remove whitespace
@@ -136,7 +139,9 @@ function parse(url, default)
end) end)
-- get scheme -- get scheme
url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
function(s) parsed.scheme = s; return "" end) function(s)
parsed.scheme = s; return ""
end)
-- get authority -- get authority
url = string.gsub(url, "^//([^/]*)", function(n) url = string.gsub(url, "^//([^/]*)", function(n)
parsed.authority = n parsed.authority = n
@@ -156,15 +161,21 @@ function parse(url, default)
if url ~= "" then parsed.path = url end if url ~= "" then parsed.path = url end
local authority = parsed.authority local authority = parsed.authority
if not authority then return parsed end if not authority then return parsed end
authority = string.gsub(authority,"^([^@]*)@", authority = string.gsub(authority, "^([^@]*)@",
function(u) parsed.userinfo = u; return "" end) function(u)
parsed.userinfo = u; return ""
end)
authority = string.gsub(authority, ":([^:]*)$", authority = string.gsub(authority, ":([^:]*)$",
function(p) parsed.port = p; return "" end) function(p)
parsed.port = p; return ""
end)
if authority ~= "" then parsed.host = authority end if authority ~= "" then parsed.host = authority end
local userinfo = parsed.userinfo local userinfo = parsed.userinfo
if not userinfo then return parsed end if not userinfo then return parsed end
userinfo = string.gsub(userinfo, ":([^:]*)$", userinfo = string.gsub(userinfo, ":([^:]*)$",
function(p) parsed.password = p; return "" end) function(p)
parsed.password = p; return ""
end)
parsed.user = userinfo parsed.user = userinfo
return parsed return parsed
end end
@@ -218,9 +229,12 @@ function absolute(base_url, relative_url)
base_parsed = parse(base_url) base_parsed = parse(base_url)
end end
local relative_parsed = parse(relative_url) local relative_parsed = parse(relative_url)
if not base_parsed then return relative_url if not base_parsed then
elseif not relative_parsed then return base_url return relative_url
elseif relative_parsed.scheme then return relative_url elseif not relative_parsed then
return base_url
elseif relative_parsed.scheme then
return relative_url
else else
relative_parsed.scheme = base_parsed.scheme relative_parsed.scheme = base_parsed.scheme
if not relative_parsed.authority then if not relative_parsed.authority then
@@ -253,7 +267,7 @@ function parse_path(path)
local parsed = {} local parsed = {}
path = path or "" path = path or ""
--path = string.gsub(path, "%s", "") --path = string.gsub(path, "%s", "")
string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) string.gsub(path, "([^/]+)", function(s) table.insert(parsed, s) end)
for i = 1, #parsed do for i = 1, #parsed do
parsed[i] = unescape(parsed[i]) parsed[i] = unescape(parsed[i])
end end
@@ -274,7 +288,7 @@ function build_path(parsed, unsafe)
local path = "" local path = ""
local n = #parsed local n = #parsed
if unsafe then if unsafe then
for i = 1, n-1 do for i = 1, n - 1 do
path = path .. parsed[i] path = path .. parsed[i]
path = path .. "/" path = path .. "/"
end end
@@ -283,7 +297,7 @@ function build_path(parsed, unsafe)
if parsed.is_directory then path = path .. "/" end if parsed.is_directory then path = path .. "/" end
end end
else else
for i = 1, n-1 do for i = 1, n - 1 do
path = path .. protect_segment(parsed[i]) path = path .. protect_segment(parsed[i])
path = path .. "/" path = path .. "/"
end end

View File

@@ -4,7 +4,9 @@
-- Utility: Convert a list of strings into a map of string->true -- Utility: Convert a list of strings into a map of string->true
local function tmap(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end local function tmap(t)
local u = {}; for _, n in ipairs(t) do u[n] = true end; return u;
end
-- Save banned global variables for wrapping with safe functions -- Save banned global variables for wrapping with safe functions
local old_io = io local old_io = io
@@ -50,13 +52,13 @@ local allowed_dirs_readonly = tmap {
-- so this is just a precaution. -- so this is just a precaution.
local disallowed_exts = tmap { local disallowed_exts = tmap {
-- windows -- windows
'bat','bin','cab','cmd','com','cpl','ex_','exe','gadget','inf','ins','inx','isu', 'bat', 'bin', 'cab', 'cmd', 'com', 'cpl', 'ex_', 'exe', 'gadget', 'inf', 'ins', 'inx', 'isu',
'job','jse','lnk','msc','msi','msp','mst','paf','pif','ps1','reg','rgs','scr', 'job', 'jse', 'lnk', 'msc', 'msi', 'msp', 'mst', 'paf', 'pif', 'ps1', 'reg', 'rgs', 'scr',
'sct','shb','shs','u3p','vb','vbe','vbs','vbscript','ws','wsf','wsh', 'sct', 'shb', 'shs', 'u3p', 'vb', 'vbe', 'vbs', 'vbscript', 'ws', 'wsf', 'wsh',
-- linux -- linux
'csh','ksh','out','run','sh', 'csh', 'ksh', 'out', 'run', 'sh',
-- mac/other -- mac/other
'action','apk','app','command','ipa','osx','prg','workflow', 'action', 'apk', 'app', 'command', 'ipa', 'osx', 'prg', 'workflow',
} }
-- Arguments: file name (relative to game directory), boolean true if only reading -- Arguments: file name (relative to game directory), boolean true if only reading
-- Return: clean file path if allowed (or nil if disallowed), -- Return: clean file path if allowed (or nil if disallowed),
@@ -68,27 +70,27 @@ local function safe_path(fn, readonly)
-- whitelist characters -- whitelist characters
local ic = fn:find('[^a-zA-Z0-9_%-/ %.]') local ic = fn:find('[^a-zA-Z0-9_%-/ %.]')
if ic then if ic then
return nil, 'Filename \''..fn..'\' contains invalid character \''.. return nil, 'Filename \'' .. fn .. '\' contains invalid character \'' ..
fn:sub(ic, ic)..'\' at position '..ic fn:sub(ic, ic) .. '\' at position ' .. ic
end end
-- disallow up-dirs, absolute paths, and relative paths -- disallow up-dirs, absolute paths, and relative paths
-- './' and '../' are possible in scripts, because they're processed into -- './' and '../' are possible in scripts, because they're processed into
-- absolute paths in util.lua before reaching here -- absolute paths in util.lua before reaching here
if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then
return nil, 'Filename \''..fn..'\' contains invalid sequence' return nil, 'Filename \'' .. fn .. '\' contains invalid sequence'
end end
-- allow only whitelisted dirs -- allow only whitelisted dirs
local dir = fn:match('^([^/]+)/') local dir = fn:match('^([^/]+)/')
if (not dir) or ( if (not dir) or (
(not allowed_dirs[dir:lower()]) and (not allowed_dirs[dir:lower()]) and
((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then ((not readonly) or (not allowed_dirs_readonly[dir:lower()]))) then
return nil, 'filename is in disallowed directory '..(dir or 'nil') return nil, 'filename is in disallowed directory ' .. (dir or 'nil')
end end
-- disallow blacklisted extensions or no extension -- disallow blacklisted extensions or no extension
local ext = fn:match('%.([^/%.]+)$') local ext = fn:match('%.([^/%.]+)$')
if (not ext) or (disallowed_exts[ext:lower()]) then if (not ext) or (disallowed_exts[ext:lower()]) then
return nil, 'Filename \''..fn..'\' has disallowed extension \''.. return nil, 'Filename \'' .. fn .. '\' has disallowed extension \'' ..
(ext or '')..'\'' (ext or '') .. '\''
end end
return fn, nil return fn, nil
end end
@@ -96,7 +98,7 @@ end
-- Wrap io.open with path sanitization -- Wrap io.open with path sanitization
function _bllua_io_open(fn, md) function _bllua_io_open(fn, md)
md = md or 'r' md = md or 'r'
local readonly = md=='r' or md=='rb' local readonly = md == 'r' or md == 'rb'
local fns, err = safe_path(fn, readonly) local fns, err = safe_path(fn, readonly)
if fns then if fns then
return old_io.open(fns, md) return old_io.open(fns, md)
@@ -104,6 +106,7 @@ function _bllua_io_open(fn, md)
return nil, err return nil, err
end end
end end
-- Allow io.type (works on file handles returned by io.open) -- Allow io.type (works on file handles returned by io.open)
function _bllua_io_type(f) function _bllua_io_type(f)
return old_io.type(f) return old_io.type(f)
@@ -122,12 +125,13 @@ function _bllua_requiresecure(name)
name:find('^%.') or name:find('%.$') then name:find('^%.') or name:find('%.$') then
error('require: package name contains invalid character', 3) error('require: package name contains invalid character', 3)
elseif disallowed_packages[name] then elseif disallowed_packages[name] then
error('require: attempt to require disallowed module \''..name..'\'', 3) error('require: attempt to require disallowed module \'' .. name .. '\'', 3)
else else
-- todo: reimplement require to not use package.* stuff? -- todo: reimplement require to not use package.* stuff?
return old_require(name) return old_require(name)
end end
end end
package = { package = {
seeall = old_package.seeall, seeall = old_package.seeall,
} }

View File

@@ -9,7 +9,7 @@ _bllua_on_unload = {}
-- Utility for getting the current filename -- Utility for getting the current filename
function debug.getfilename(level) function debug.getfilename(level)
if type(level) == 'number' then level = level+1 end if type(level) == 'number' then level = level + 1 end
local info = debug.getinfo(level) local info = debug.getinfo(level)
if not info then return nil end if not info then return nil end
local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]') local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]')
@@ -19,20 +19,20 @@ end
-- Called when pcall fails on a ts->lua call, used to print detailed error info -- Called when pcall fails on a ts->lua call, used to print detailed error info
function _bllua_on_error(err) function _bllua_on_error(err)
err = err:match(': (.+)$') or err err = err:match(': (.+)$') or err
local tracelines = {err} local tracelines = { err }
local level = 2 local level = 2
while true do while true do
local info = debug.getinfo(level) local info = debug.getinfo(level)
if not info then break end if not info then break end
local filename = debug.getfilename(level) or info.short_src local filename = debug.getfilename(level) or info.short_src
local funcname = info.name local funcname = info.name
if funcname=='dofile' then break end if funcname == 'dofile' then break end
table.insert(tracelines, string.format('%s:%s in function \'%s\'', table.insert(tracelines, string.format('%s:%s in function \'%s\'',
filename, filename,
info.currentline==-1 and '' or info.currentline..':', info.currentline == -1 and '' or info.currentline .. ':',
funcname funcname
)) ))
level = level+1 level = level + 1
end end
return table.concat(tracelines, '\n') return table.concat(tracelines, '\n')
end end

View File

@@ -4,129 +4,129 @@
-- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy -- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy
bl.class('SimObject') bl.class('SimObject')
bl.class('ScriptObject', 'SimObject') bl.class('ScriptObject', 'SimObject')
bl.class('SimSet', 'SimObject') bl.class('SimSet', 'SimObject')
bl.class('SimGroup', 'SimSet') bl.class('SimGroup', 'SimSet')
bl.class('GuiControl', 'SimGroup') bl.class('GuiControl', 'SimGroup')
bl.class('GuiTextCtrl' , 'GuiControl') bl.class('GuiTextCtrl', 'GuiControl')
bl.class('GuiSwatchCtrl' , 'GuiControl') bl.class('GuiSwatchCtrl', 'GuiControl')
bl.class('GuiButtonBaseCtrl' , 'GuiControl') bl.class('GuiButtonBaseCtrl', 'GuiControl')
bl.class('GuiArrayCtrl' , 'GuiControl') bl.class('GuiArrayCtrl', 'GuiControl')
bl.class('GuiScrollCtrl' , 'GuiControl') bl.class('GuiScrollCtrl', 'GuiControl')
bl.class('GuiMouseEventCtrl' , 'GuiControl') bl.class('GuiMouseEventCtrl', 'GuiControl')
bl.class('GuiProgressCtrl' , 'GuiControl') bl.class('GuiProgressCtrl', 'GuiControl')
bl.class('GuiSliderCtrl' , 'GuiControl') bl.class('GuiSliderCtrl', 'GuiControl')
bl.class('GuiConsoleTextCtrl' , 'GuiControl') bl.class('GuiConsoleTextCtrl', 'GuiControl')
bl.class('GuiTSCtrl' , 'GuiControl') bl.class('GuiTSCtrl', 'GuiControl')
bl.class('GuiObjectView', 'GuiTSCtrl') bl.class('GuiObjectView', 'GuiTSCtrl')
bl.class('GameTSCtrl' , 'GuiTSCtrl') bl.class('GameTSCtrl', 'GuiTSCtrl')
bl.class('EditTSCtrl' , 'GuiTSCtrl') bl.class('EditTSCtrl', 'GuiTSCtrl')
bl.class('GuiPlayerView', 'GuiTSCtrl') bl.class('GuiPlayerView', 'GuiTSCtrl')
bl.class('GuiShapeNameHud' , 'GuiControl') bl.class('GuiShapeNameHud', 'GuiControl')
bl.class('GuiHealthBarHud' , 'GuiControl') bl.class('GuiHealthBarHud', 'GuiControl')
bl.class('GuiGraphCtrl' , 'GuiControl') bl.class('GuiGraphCtrl', 'GuiControl')
bl.class('GuiInspector' , 'GuiControl') bl.class('GuiInspector', 'GuiControl')
bl.class('GuiChunkedBitmapCtrl', 'GuiControl') bl.class('GuiChunkedBitmapCtrl', 'GuiControl')
bl.class('GuiInputCtrl' , 'GuiControl') bl.class('GuiInputCtrl', 'GuiControl')
bl.class('GuiNoMouseCtrl' , 'GuiControl') bl.class('GuiNoMouseCtrl', 'GuiControl')
bl.class('GuiBitmapBorderCtrl' , 'GuiControl') bl.class('GuiBitmapBorderCtrl', 'GuiControl')
bl.class('GuiBackgroundCtrl' , 'GuiControl') bl.class('GuiBackgroundCtrl', 'GuiControl')
bl.class('GuiEditorRuler' , 'GuiControl') bl.class('GuiEditorRuler', 'GuiControl')
bl.class('GuiClockHud' , 'GuiControl') bl.class('GuiClockHud', 'GuiControl')
bl.class('GuiEditCtrl' , 'GuiControl') bl.class('GuiEditCtrl', 'GuiControl')
bl.class('GuiFilterCtrl' , 'GuiControl') bl.class('GuiFilterCtrl', 'GuiControl')
bl.class('GuiFrameSetCtrl' , 'GuiControl') bl.class('GuiFrameSetCtrl', 'GuiControl')
bl.class('GuiMenuBar' , 'GuiControl') bl.class('GuiMenuBar', 'GuiControl')
bl.class('GuiMessageVectorCtrl', 'GuiControl') bl.class('GuiMessageVectorCtrl', 'GuiControl')
bl.class('GuiBitmapCtrl' , 'GuiControl') bl.class('GuiBitmapCtrl', 'GuiControl')
bl.class('GuiCrossHairHud', 'GuiBitmapCtrl') bl.class('GuiCrossHairHud', 'GuiBitmapCtrl')
bl.class('ScriptGroup', 'SimGroup') bl.class('ScriptGroup', 'SimGroup')
bl.class('NetConnection', 'SimGroup') bl.class('NetConnection', 'SimGroup')
bl.class('GameConnection', 'NetConnection') bl.class('GameConnection', 'NetConnection')
bl.class('Path', 'SimGroup') bl.class('Path', 'SimGroup')
bl.class('TCPObject', 'SimObject') bl.class('TCPObject', 'SimObject')
bl.class('SOCKObject', 'TCPObject') bl.class('SOCKObject', 'TCPObject')
bl.class('HTTPObject', 'TCPObject') bl.class('HTTPObject', 'TCPObject')
bl.class('SimDataBlock', 'SimObject') bl.class('SimDataBlock', 'SimObject')
bl.class('AudioEnvironment' , 'SimDataBlock') bl.class('AudioEnvironment', 'SimDataBlock')
bl.class('AudioSampleEnvironment', 'SimDataBlock') bl.class('AudioSampleEnvironment', 'SimDataBlock')
bl.class('AudioDescription' , 'SimDataBlock') bl.class('AudioDescription', 'SimDataBlock')
bl.class('GameBaseData' , 'SimDataBlock') bl.class('GameBaseData', 'SimDataBlock')
bl.class('ShapeBaseData' , 'GameBaseData') bl.class('ShapeBaseData', 'GameBaseData')
bl.class('CameraData' , 'ShapeBaseData') bl.class('CameraData', 'ShapeBaseData')
bl.class('ItemData' , 'ShapeBaseData') bl.class('ItemData', 'ShapeBaseData')
bl.class('MissionMarkerData', 'ShapeBaseData') bl.class('MissionMarkerData', 'ShapeBaseData')
bl.class('PathCameraData' , 'ShapeBaseData') bl.class('PathCameraData', 'ShapeBaseData')
bl.class('PlayerData' , 'ShapeBaseData') bl.class('PlayerData', 'ShapeBaseData')
bl.class('StaticShapeData' , 'ShapeBaseData') bl.class('StaticShapeData', 'ShapeBaseData')
bl.class('VehicleData' , 'ShapeBaseData') bl.class('VehicleData', 'ShapeBaseData')
bl.class('FlyingVehicleData' , 'VehicleData') bl.class('FlyingVehicleData', 'VehicleData')
bl.class('WheeledVehicleData', 'VehicleData') bl.class('WheeledVehicleData', 'VehicleData')
bl.class('DebrisData' , 'GameBaseData') bl.class('DebrisData', 'GameBaseData')
bl.class('ProjectileData' , 'GameBaseData') bl.class('ProjectileData', 'GameBaseData')
bl.class('ShapeBaseImageData' , 'GameBaseData') bl.class('ShapeBaseImageData', 'GameBaseData')
bl.class('TriggerData' , 'GameBaseData') bl.class('TriggerData', 'GameBaseData')
bl.class('ExplosionData' , 'GameBaseData') bl.class('ExplosionData', 'GameBaseData')
bl.class('fxLightData' , 'GameBaseData') bl.class('fxLightData', 'GameBaseData')
bl.class('LightningData' , 'GameBaseData') bl.class('LightningData', 'GameBaseData')
bl.class('ParticleEmitterNodeData', 'GameBaseData') bl.class('ParticleEmitterNodeData', 'GameBaseData')
bl.class('SplashData' , 'GameBaseData') bl.class('SplashData', 'GameBaseData')
bl.class('fxDTSBrickData' , 'GameBaseData') bl.class('fxDTSBrickData', 'GameBaseData')
bl.class('ParticleEmitterData' , 'GameBaseData') bl.class('ParticleEmitterData', 'GameBaseData')
bl.class('WheeledVehicleTire' , 'SimDataBlock') bl.class('WheeledVehicleTire', 'SimDataBlock')
bl.class('WheeledVehicleSpring' , 'SimDataBlock') bl.class('WheeledVehicleSpring', 'SimDataBlock')
bl.class('TSShapeConstructor' , 'SimDataBlock') bl.class('TSShapeConstructor', 'SimDataBlock')
bl.class('AudioProfile' , 'SimDataBlock') bl.class('AudioProfile', 'SimDataBlock')
bl.class('ParticleData' , 'SimDataBlock') bl.class('ParticleData', 'SimDataBlock')
bl.class('MaterialPropertyMap', 'SimObject') bl.class('MaterialPropertyMap', 'SimObject')
bl.class('NetObject', 'SimObject') bl.class('NetObject', 'SimObject')
bl.class('SceneObject', 'NetObject') bl.class('SceneObject', 'NetObject')
bl.class('GameBase', 'SceneObject') bl.class('GameBase', 'SceneObject')
bl.class('ShapeBase', 'GameBase') bl.class('ShapeBase', 'GameBase')
bl.class('MissionMarker', 'ShapeBase') bl.class('MissionMarker', 'ShapeBase')
bl.class('SpawnSphere' , 'MissionMarker') bl.class('SpawnSphere', 'MissionMarker')
bl.class('VehicleSpawnMarker', 'MissionMarker') bl.class('VehicleSpawnMarker', 'MissionMarker')
bl.class('Waypoint' , 'MissionMarker') bl.class('Waypoint', 'MissionMarker')
bl.class('StaticShape' , 'ShapeBase') bl.class('StaticShape', 'ShapeBase')
bl.class('ScopeAlwaysShape', 'StaticShape') bl.class('ScopeAlwaysShape', 'StaticShape')
bl.class('Player' , 'ShapeBase') bl.class('Player', 'ShapeBase')
bl.class('AIPlayer', 'Player') bl.class('AIPlayer', 'Player')
bl.class('Camera' , 'ShapeBase') bl.class('Camera', 'ShapeBase')
bl.class('Item' , 'ShapeBase') bl.class('Item', 'ShapeBase')
bl.class('PathCamera' , 'ShapeBase') bl.class('PathCamera', 'ShapeBase')
bl.class('Vehicle' , 'ShapeBase') bl.class('Vehicle', 'ShapeBase')
bl.class('FlyingVehicle' , 'Vehicle') bl.class('FlyingVehicle', 'Vehicle')
bl.class('WheeledVehicle', 'Vehicle') bl.class('WheeledVehicle', 'Vehicle')
bl.class('Explosion' , 'GameBase') bl.class('Explosion', 'GameBase')
bl.class('Splash' , 'GameBase') bl.class('Splash', 'GameBase')
bl.class('Debris' , 'GameBase') bl.class('Debris', 'GameBase')
bl.class('Projectile' , 'GameBase') bl.class('Projectile', 'GameBase')
bl.class('Trigger' , 'GameBase') bl.class('Trigger', 'GameBase')
bl.class('fxLight' , 'GameBase') bl.class('fxLight', 'GameBase')
bl.class('Lightning' , 'GameBase') bl.class('Lightning', 'GameBase')
bl.class('ParticleEmitterNode', 'GameBase') bl.class('ParticleEmitterNode', 'GameBase')
bl.class('ParticleEmitter' , 'GameBase') bl.class('ParticleEmitter', 'GameBase')
bl.class('Precipitation' , 'GameBase') bl.class('Precipitation', 'GameBase')
bl.class('TSStatic' , 'SceneObject') bl.class('TSStatic', 'SceneObject')
bl.class('VehicleBlocker', 'SceneObject') bl.class('VehicleBlocker', 'SceneObject')
bl.class('Marker' , 'SceneObject') bl.class('Marker', 'SceneObject')
bl.class('AudioEmitter' , 'SceneObject') bl.class('AudioEmitter', 'SceneObject')
bl.class('PhysicalZone' , 'SceneObject') bl.class('PhysicalZone', 'SceneObject')
bl.class('fxDayCycle' , 'SceneObject') bl.class('fxDayCycle', 'SceneObject')
bl.class('fxDTSBrick' , 'SceneObject') bl.class('fxDTSBrick', 'SceneObject')
bl.class('fxPlane' , 'SceneObject') bl.class('fxPlane', 'SceneObject')
bl.class('fxSunLight' , 'SceneObject') bl.class('fxSunLight', 'SceneObject')
bl.class('Sky' , 'SceneObject') bl.class('Sky', 'SceneObject')
bl.class('SceneRoot' , 'SceneObject') bl.class('SceneRoot', 'SceneObject')
bl.class('Sun', 'NetObject') bl.class('Sun', 'NetObject')
bl.class('GuiCursor', 'SimObject') bl.class('GuiCursor', 'SimObject')
bl.class('ConsoleLogger' , 'SimObject') bl.class('ConsoleLogger', 'SimObject')
bl.class('QuotaObject' , 'SimObject') bl.class('QuotaObject', 'SimObject')
bl.class('FileObject' , 'SimObject') bl.class('FileObject', 'SimObject')
bl.class('BanList' , 'SimObject') bl.class('BanList', 'SimObject')
bl.class('GuiControlProfile', 'SimObject') bl.class('GuiControlProfile', 'SimObject')
bl.class('MessageVector' , 'SimObject') bl.class('MessageVector', 'SimObject')
bl.class('ActionMap' , 'SimObject') bl.class('ActionMap', 'SimObject')
-- Auto-generated from game scripts -- Auto-generated from game scripts
bl.type('ActionMap::blockBind:1', 'object') bl.type('ActionMap::blockBind:1', 'object')

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
-- This Lua code provides some built-in utilities for writing Lua add-ons -- This Lua code provides some built-in utilities for writing Lua add-ons
-- It is eval'd automatically once BLLua3 has loaded the TS API and environment -- It is eval'd automatically once BLLua3 has loaded the TS API and environment
-- It only has access to the sandboxed lua environment, just like user code. -- It only has access to the sandboxed lua environment, just like user code.
@@ -8,9 +7,10 @@ ts = _bllua_ts
-- Provide limited OS functions -- Provide limited OS functions
os = os or {} os = os or {}
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime'))/1000) end function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime')) / 1000) end
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end function os.clock() return tonumber(_bllua_ts.call('getSimTime')) / 1000 end
-- Virtual file class, emulating a file object as returned by io.open -- Virtual file class, emulating a file object as returned by io.open
-- Used to wrap io.open to allow reading from zips (using TS) -- Used to wrap io.open to allow reading from zips (using TS)
@@ -20,9 +20,9 @@ function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end
local file_meta = { local file_meta = {
read = function(file, mode) read = function(file, mode)
file:_init() file:_init()
if not file or type(file)~='table' or not file._is_file then error('File:read: Not a file', 2) end if not file or type(file) ~= 'table' or not file._is_file then error('File:read: Not a file', 2) end
if file._is_open ~= true then error('File:read: File is closed', 2) end if file._is_open ~= true then error('File:read: File is closed', 2) end
if mode=='*n' then if mode == '*n' then
local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos)
if n then if n then
file.pos = file.pos + #ws + #n file.pos = file.pos + #ws + #n
@@ -30,15 +30,15 @@ local file_meta = {
else else
return nil return nil
end end
elseif mode=='*a' then elseif mode == '*a' then
local d = file.data:sub(file.pos, #file.data) local d = file.data:sub(file.pos, #file.data)
file.pos = #file.data + 1 file.pos = #file.data + 1
return d return d
elseif mode=='*l' then elseif mode == '*l' then
local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos)
if not l then if not l then
l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; l = file.data:match('^([^\r\n]*)$', file.pos); ws = '';
if l=='' then return nil end if l == '' then return nil end
end end
if l then if l then
file.pos = file.pos + #l + #ws file.pos = file.pos + #l + #ws
@@ -46,12 +46,12 @@ local file_meta = {
else else
return nil return nil
end end
elseif type(mode)=='number' then elseif type(mode) == 'number' then
local d = file.data:sub(file.pos, file.pos+mode) local d = file.data:sub(file.pos, file.pos + mode)
file.pos = file.pos + #d file.pos = file.pos + #d
return d return d
else else
error('File:read: Invalid mode \''..mode..'\'', 2) error('File:read: Invalid mode \'' .. mode .. '\'', 2)
end end
end, end,
lines = function(file) lines = function(file)
@@ -84,8 +84,10 @@ local function new_file_obj(fn)
return file return file
end end
local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end local function tflip(t)
local allowed_zip_dirs = tflip{ local u = {}; for _, n in ipairs(t) do u[n] = true end; return u;
end
local allowed_zip_dirs = tflip {
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
} }
local function io_open_absolute(fn, mode) local function io_open_absolute(fn, mode)
@@ -99,8 +101,9 @@ local function io_open_absolute(fn, mode)
local exist = _bllua_ts.call('isFile', fn) == '1' local exist = _bllua_ts.call('isFile', fn) == '1'
if not exist then return nil, err end if not exist then return nil, err end
if mode~=nil and mode~='r' and mode~='rb' then if mode ~= nil and mode ~= 'r' and mode ~= 'rb' then
return nil, 'Files in zips can only be opened in read mode' end return nil, 'Files in zips can only be opened in read mode'
end
-- return a temp lua file object with the data -- return a temp lua file object with the data
local fi = new_file_obj(fn) local fi = new_file_obj(fn)
@@ -117,7 +120,7 @@ function io.open(fn, mode, errn)
if curfn == '' then curfn = nil end if curfn == '' then curfn = nil end
if fn:find('^%.') then if fn:find('^%.') then
local relfn = curfn and fn:find('^%./') and local relfn = curfn and fn:find('^%./') and
curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '') curfn:gsub('[^/]+$', '') .. fn:gsub('^%./', '')
if relfn then if relfn then
local fi, err = io_open_absolute(relfn, mode) local fi, err = io_open_absolute(relfn, mode)
return fi, err, relfn return fi, err, relfn
@@ -129,17 +132,19 @@ function io.open(fn, mode, errn)
return fi, err, fn return fi, err, fn
end end
end end
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function io.lines(fn) function io.lines(fn)
local fi, err, fn2 = io.open(fn, nil, 2) local fi, err, fn2 = io.open(fn, nil, 2)
if not fi then error('Error opening file \''..fn2..'\': '..err, 2) end if not fi then error('Error opening file \'' .. fn2 .. '\': ' .. err, 2) end
return fi:lines() return fi:lines()
end end
---@diagnostic disable-next-line: duplicate-set-field ---@diagnostic disable-next-line: duplicate-set-field
function io.type(f) function io.type(f)
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
if type(f)=='table' and f._is_file then if type(f) == 'table' and f._is_file then
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
return f._is_open and 'file' or 'closed file' return f._is_open and 'file' or 'closed file'
else else
return _bllua_io_type(f) return _bllua_io_type(f)
@@ -150,13 +155,13 @@ end
function dofile(fn, errn) function dofile(fn, errn)
errn = errn or 1 errn = errn or 1
local fi, err, fn2 = io.open(fn, 'r', errn+1) local fi, err, fn2 = io.open(fn, 'r', errn + 1)
if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end if not fi then error('Error executing file \'' .. fn2 .. '\': ' .. err, errn + 1) end
print('Executing '..fn2) print('Executing ' .. fn2)
local text = fi:read('*a') local text = fi:read('*a')
fi:close() fi:close()
return assert(loadstring('--[['..fn2..']]'..text))() return assert(loadstring('--[[' .. fn2 .. ']]' .. text))()
end end
-- provide require (just a wrapper for dofile) -- provide require (just a wrapper for dofile)
@@ -165,7 +170,7 @@ end
-- blockland directory -- blockland directory
-- current add-on -- current add-on
local function file_exists(fn, errn) local function file_exists(fn, errn)
local fi, err, fn2 = io.open(fn, 'r', errn+1) local fi, err, fn2 = io.open(fn, 'r', errn + 1)
if fi then if fi then
fi:close() fi:close()
return fn2 return fn2
@@ -178,20 +183,20 @@ function require(mod)
if require_memo[mod] then return unpack(require_memo[mod]) end if require_memo[mod] then return unpack(require_memo[mod]) end
local fp = mod:gsub('%.', '/') local fp = mod:gsub('%.', '/')
local fns = { local fns = {
'./'..fp..'.lua', -- local file './' .. fp .. '.lua', -- local file
'./'..fp..'/init.lua', -- local library './' .. fp .. '/init.lua', -- local library
fp..'.lua', -- global file fp .. '.lua', -- global file
fp..'/init.lua', -- global library fp .. '/init.lua', -- global library
} }
if fp:lower():find('^add-ons/') then if fp:lower():find('^add-ons/') then
local addonpath = fp:lower():match('^add-ons/[^/]+')..'/' local addonpath = fp:lower():match('^add-ons/[^/]+') .. '/'
table.insert(fns, addonpath..fp..'.lua') -- add-on file table.insert(fns, addonpath .. fp .. '.lua') -- add-on file
table.insert(fns, addonpath..fp..'/init.lua') -- add-on library table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library
end end
for _,fn in ipairs(fns) do for _, fn in ipairs(fns) do
local fne = file_exists(fn, 2) local fne = file_exists(fn, 2)
if fne then if fne then
local res = {dofile(fne, 2)} local res = { dofile(fne, 2) }
require_memo[mod] = res require_memo[mod] = res
return unpack(res) return unpack(res)
end end
@@ -201,18 +206,22 @@ end
-- Exposure to TS -- Exposure to TS
function _bllua_getvar(name) return _G[name] end function _bllua_getvar(name) return _G[name] end
function _bllua_setvar(name, val) _G[name] = val end function _bllua_setvar(name, val) _G[name] = val end
function _bllua_eval(code) return loadstring(code)() end function _bllua_eval(code) return loadstring(code)() end
function _bllua_exec(fn) return dofile(fn, 2) end function _bllua_exec(fn) return dofile(fn, 2) end
local function isValidCode(code) local function isValidCode(code)
local f,e = loadstring(code) local f, e = loadstring(code)
return f~=nil return f ~= nil
end end
function _bllua_smarteval(code) function _bllua_smarteval(code)
if (not code:find('^print%(')) and isValidCode('print('..code..')') then if (not code:find('^print%(')) and isValidCode('print(' .. code .. ')') then
code = 'print('..code..')' end code = 'print(' .. code .. ')'
local f,e = loadstring(code) end
local f, e = loadstring(code)
if f then if f then
return f() return f()
else else

View File

@@ -1,263 +1,285 @@
-- Basic functionality that should be standard in Lua -- Basic functionality that should be standard in Lua
-- Table / List -- Table / List
-- Whether a table contains no keys -- Whether a table contains no keys
function table.empty(t) function table.empty(t)
return next(t)~=nil return next(t) ~= nil
end end
-- Apply a function to each key in a table -- Apply a function to each key in a table
function table.map(f, ...) function table.map(f, ...)
local ts = {...} local ts = { ... }
local u = {} local u = {}
for k,_ in pairs(ts[1]) do for k, _ in pairs(ts[1]) do
local args = {} local args = {}
for j=1,#ts do args[j] = ts[j][i] end for j = 1, #ts do args[j] = ts[j][i] end
u[i] = f(unpack(args)) u[i] = f(unpack(args))
end end
return u return u
end end
function table.map_list(f, ...) function table.map_list(f, ...)
local ts = {...} local ts = { ... }
local u = {} local u = {}
for i=1,#ts[1] do for i = 1, #ts[1] do
local args = {} local args = {}
for j=1,#ts do args[j] = ts[j][i] end for j = 1, #ts do args[j] = ts[j][i] end
u[i] = f(unpack(args)) u[i] = f(unpack(args))
end end
return u return u
end end
-- Swap keys/values -- Swap keys/values
function table.swap(t) function table.swap(t)
local u = {} local u = {}
for k,v in pairs(t) do u[v] = k end for k, v in pairs(t) do u[v] = k end
return u return u
end end
-- Reverse a list -- Reverse a list
function table.reverse(l) function table.reverse(l)
local m = {} local m = {}
for i=1,#l do m[#l-i+1] = l[i] end for i = 1, #l do m[#l - i + 1] = l[i] end
return m return m
end end
-- Whether a table is a list/array (has only monotonic integer keys) -- Whether a table is a list/array (has only monotonic integer keys)
function table.islist(t) function table.islist(t)
local n = 0 local n = 0
for i,_ in pairs(t) do for i, _ in pairs(t) do
if type(i)~='number' or i%1~=0 then return false end if type(i) ~= 'number' or i % 1 ~= 0 then return false end
n = n+1 n = n + 1
end end
return n==#t return n == #t
end end
-- Append contents of other tables to first table -- Append contents of other tables to first table
function table.append(t, ...) function table.append(t, ...)
local a = {...} local a = { ... }
for _,u in ipairs(a) do for _, u in ipairs(a) do
for _,v in ipairs(u) do table.insert(t,v) end for _, v in ipairs(u) do table.insert(t, v) end
end end
return t return t
end end
-- Create a new table containing all keys from any number of tables -- Create a new table containing all keys from any number of tables
-- latter tables in the arg list override prior ones -- latter tables in the arg list override prior ones
-- overlaps, NOT appends, integer keys -- overlaps, NOT appends, integer keys
function table.join(...) function table.join(...)
local ts = {...} local ts = { ... }
local w = {} local w = {}
for _,t in ipairs(ts) do for _, t in ipairs(ts) do
for k,v in pairs(t) do w[k] = v end for k, v in pairs(t) do w[k] = v end
end end
return w return w
end end
-- Whether a table contains a certain value in any key -- Whether a table contains a certain value in any key
function table.contains(t,s) function table.contains(t, s)
for _,v in pairs(t) do for _, v in pairs(t) do
if v==s then return true end if v == s then return true end
end end
return false return false
end end
function table.contains_list(t,s)
for _,v in ipairs(t) do function table.contains_list(t, s)
if v==s then return true end for _, v in ipairs(t) do
if v == s then return true end
end end
return false return false
end end
-- Copy a table to another table -- Copy a table to another table
function table.copy(t) function table.copy(t)
local u = {} local u = {}
for k,v in pairs(t) do u[k] = v end for k, v in pairs(t) do u[k] = v end
return u return u
end end
function table.copy_list(l) function table.copy_list(l)
local m = {} local m = {}
for i,v in ipairs(l) do m[i] = v end for i, v in ipairs(l) do m[i] = v end
return m return m
end end
-- Sort a table in a new copy -- Sort a table in a new copy
function table.sortcopy(t, f) function table.sortcopy(t, f)
local u = table.copy_list(t) local u = table.copy_list(t)
table.sort(u, f) table.sort(u, f)
return u return u
end end
-- Remove a value from a table -- Remove a value from a table
function table.removevalue(t, r) function table.removevalue(t, r)
local rem = {} local rem = {}
for k,v in pairs(t) do for k, v in pairs(t) do
if v==r then table.insert(rem, k) end if v == r then table.insert(rem, k) end
end end
for _,k in ipairs(rem) do t[k] = nil end for _, k in ipairs(rem) do t[k] = nil end
end end
function table.removevalue_list(t, r) function table.removevalue_list(t, r)
for i = #t, 1, -1 do for i = #t, 1, -1 do
if t[i]==r then if t[i] == r then
table.remove(t, i) table.remove(t, i)
end end
end end
end end
-- Export tables into formatted executable strings -- Export tables into formatted executable strings
local function tabs(tabLevel) local function tabs(tabLevel)
return (' '):rep(tabLevel) return (' '):rep(tabLevel)
end end
local valueToString local valueToString
local function tableToString(t, tabLevel, seen) local function tableToString(t, tabLevel, seen)
if type(t)~='table' or (getmetatable(t) and getmetatable(t).__tostring) then if type(t) ~= 'table' or (getmetatable(t) and getmetatable(t).__tostring) then
return tostring(t) return tostring(t)
elseif table.islist(t) then elseif table.islist(t) then
if #t==0 then if #t == 0 then
return '{}' return '{}'
else else
local strs = {} local strs = {}
local containsTables = false local containsTables = false
for _,v in ipairs(t) do for _, v in ipairs(t) do
if type(v)=='table' then containsTables = true end if type(v) == 'table' then containsTables = true end
table.insert(strs, valueToString(v, tabLevel+1, seen)..',') table.insert(strs, valueToString(v, tabLevel + 1, seen) .. ',')
end end
if containsTables or #t>3 then if containsTables or #t > 3 then
return '{\n'..tabs(tabLevel+1) return '{\n' .. tabs(tabLevel + 1)
..table.concat(strs, '\n'..tabs(tabLevel+1)) .. table.concat(strs, '\n' .. tabs(tabLevel + 1))
..'\n'..tabs(tabLevel)..'}' .. '\n' .. tabs(tabLevel) .. '}'
else else
return '{ '..table.concat(strs, ' ')..' }' return '{ ' .. table.concat(strs, ' ') .. ' }'
end end
end end
else else
local containsNonStringKeys = false local containsNonStringKeys = false
for k,v in pairs(t) do for k, v in pairs(t) do
if type(k)~='string' or k:find('[^a-zA-Z0-9_]') then if type(k) ~= 'string' or k:find('[^a-zA-Z0-9_]') then
containsNonStringKeys = true containsNonStringKeys = true
elseif type(k)=='table' then elseif type(k) == 'table' then
error('table.tostring: table contains a table as key, cannot serialize') error('table.tostring: table contains a table as key, cannot serialize')
end end
end end
local strs = {} local strs = {}
if containsNonStringKeys then if containsNonStringKeys then
for k,v in pairs(t) do for k, v in pairs(t) do
table.insert(strs, '\n'..tabs(tabLevel+1) table.insert(strs, '\n' .. tabs(tabLevel + 1)
..'['..valueToString(k, tabLevel+1, seen)..'] = ' .. '[' .. valueToString(k, tabLevel + 1, seen) .. '] = '
..valueToString(v, tabLevel+1, seen)..',') .. valueToString(v, tabLevel + 1, seen) .. ',')
end end
else else
for k,v in pairs(t) do for k, v in pairs(t) do
table.insert(strs, '\n'..tabs(tabLevel+1) table.insert(strs, '\n' .. tabs(tabLevel + 1)
..k..' = '..valueToString(v, tabLevel+1, seen)..',') .. k .. ' = ' .. valueToString(v, tabLevel + 1, seen) .. ',')
end end
end end
return '{'..table.concat(strs)..'\n'..tabs(tabLevel)..'}' return '{' .. table.concat(strs) .. '\n' .. tabs(tabLevel) .. '}'
end end
end end
valueToString = function(v, tabLevel, seen) valueToString = function(v, tabLevel, seen)
local t = type(v) local t = type(v)
if t=='table' then if t == 'table' then
if seen[v] then if seen[v] then
return 'nil --[[ already seen: '..tostring(v)..' ]]' return 'nil --[[ already seen: ' .. tostring(v) .. ' ]]'
else else
seen[v] = true seen[v] = true
return tableToString(v, tabLevel, seen) return tableToString(v, tabLevel, seen)
end end
elseif t=='string' then elseif t == 'string' then
return '\''..string.escape(v)..'\'' return '\'' .. string.escape(v) .. '\''
elseif t=='number' or t=='boolean' then elseif t == 'number' or t == 'boolean' then
return tostring(v) return tostring(v)
else else
--error('table.tostring: table contains a '..t..' value, cannot serialize') --error('table.tostring: table contains a '..t..' value, cannot serialize')
return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]' return 'nil --[[ cannot serialize ' .. t .. ': ' .. tostring(v) .. ' ]]'
end end
end end
function table.tostring(t) function table.tostring(t)
return tableToString(t, 0, {}) return tableToString(t, 0, {})
end end
-- String -- String
-- Split string into table by separator -- Split string into table by separator
-- or by chars if no separator given -- or by chars if no separator given
-- if regex is not true, sep is treated as a regex pattern -- if regex is not true, sep is treated as a regex pattern
function string.split(str, sep, noregex) function string.split(str, sep, noregex)
if type(str)~='string' then if type(str) ~= 'string' then
error('string.split: argument #1: expected string, got '..type(str), 2) end error('string.split: argument #1: expected string, got ' .. type(str), 2)
if sep==nil or sep=='' then end
if sep == nil or sep == '' then
local t = {} local t = {}
local ns = #str local ns = #str
for x = 1, ns do for x = 1, ns do
table.insert(t, str:sub(x, x)) table.insert(t, str:sub(x, x))
end end
return t return t
elseif type(sep)=='string' then elseif type(sep) == 'string' then
local t = {} local t = {}
if #str>0 then if #str > 0 then
local first = 1 local first = 1
while true do while true do
local last, newfirst = str:find(sep, first, noregex) local last, newfirst = str:find(sep, first, noregex)
if not last then break end if not last then break end
table.insert(t, str:sub(first, last-1)) table.insert(t, str:sub(first, last - 1))
first = newfirst+1 first = newfirst + 1
end end
table.insert(t, str:sub(first, #str)) table.insert(t, str:sub(first, #str))
end end
return t return t
else else
error( error(
'string.split: argument #2: expected string or nil, got '..type(sep), 2) 'string.split: argument #2: expected string or nil, got ' .. type(sep), 2)
end end
end end
-- Split string to a list of char bytes -- Split string to a list of char bytes
function string.bytes(s) function string.bytes(s)
local b = {} local b = {}
for i=1,#s do for i = 1, #s do
local c = s:sub(i,i) local c = s:sub(i, i)
table.insert(b, c:byte()) table.insert(b, c:byte())
end end
return b return b
end end
-- Trim leading and trailing whitespace -- Trim leading and trailing whitespace
function string.trim(s, ws) function string.trim(s, ws)
ws = ws or ' \t\r\n' ws = ws or ' \t\r\n'
return s:gsub('^['..ws..']+', ''):gsub('['..ws..']+$', '')..'' return s:gsub('^[' .. ws .. ']+', ''):gsub('[' .. ws .. ']+$', '') .. ''
end end
-- String slicing and searching using [] operator -- String slicing and searching using [] operator
local str_meta = getmetatable('') local str_meta = getmetatable('')
local str_meta_index_old= str_meta.__index local str_meta_index_old = str_meta.__index
function str_meta.__index(s,k) function str_meta.__index(s, k)
if type(k)=='string' then if type(k) == 'string' then
return str_meta_index_old[k] return str_meta_index_old[k]
elseif type(k)=='number' then elseif type(k) == 'number' then
if k<0 then k = #s+k+1 end if k < 0 then k = #s + k + 1 end
return string.sub(s,k,k) return string.sub(s, k, k)
elseif type(k)=='table' then elseif type(k) == 'table' then
local a = k[1]<0 and (#s+k[1]+1) or k[1] local a = k[1] < 0 and (#s + k[1] + 1) or k[1]
local b = k[2]<0 and (#s+k[2]+1) or k[2] local b = k[2] < 0 and (#s + k[2] + 1) or k[2]
return string.sub(s,a,b) return string.sub(s, a, b)
end end
end end
-- String iterator -- String iterator
function string.chars(s) function string.chars(s)
local i = 0 local i = 0
return function() return function()
i = i+1 i = i + 1
if i<=#s then return s:sub(i,i) if i <= #s then
else return nil end return s:sub(i, i)
else
return nil
end
end end
end end
-- Escape sequences -- Escape sequences
local defaultEscapes = { local defaultEscapes = {
['\\'] = '\\\\', ['\\'] = '\\\\',
@@ -271,12 +293,13 @@ local defaultEscapes = {
function string.escape(s, escapes) function string.escape(s, escapes)
escapes = escapes or defaultEscapes escapes = escapes or defaultEscapes
local t = {} local t = {}
for i=1,#s do for i = 1, #s do
local c = s:sub(i,i) local c = s:sub(i, i)
table.insert(t, escapes[c] or c) table.insert(t, escapes[c] or c)
end end
return table.concat(t) return table.concat(t)
end end
local defaultEscapeChar = '\\' local defaultEscapeChar = '\\'
local defaultUnescapes = { local defaultUnescapes = {
['\\'] = '\\', ['\\'] = '\\',
@@ -292,13 +315,13 @@ function string.unescape(s, escapeChar, unescapes)
unescapes = unescapes or defaultUnescapes unescapes = unescapes or defaultUnescapes
local t = {} local t = {}
local inEscape = false local inEscape = false
for i=1,#s do for i = 1, #s do
local c = s:sub(i,i) local c = s:sub(i, i)
if inEscape then if inEscape then
table.insert(t, unescapes[c] table.insert(t, unescapes[c]
or error('string.unescape: invalid escape sequence: \'' or error('string.unescape: invalid escape sequence: \''
..escapeChar..c..'\'')) .. escapeChar .. c .. '\''))
elseif c==escapeChar then elseif c == escapeChar then
inEscape = true inEscape = true
else else
table.insert(t, c) table.insert(t, c)
@@ -307,40 +330,44 @@ function string.unescape(s, escapeChar, unescapes)
return table.concat(t) return table.concat(t)
end end
-- IO -- IO
io = io or {} io = io or {}
-- Read entire file at once, return nil,err if access failed -- Read entire file at once, return nil,err if access failed
function io.readall(filename) function io.readall(filename)
local fi,err = io.open(filename, 'rb') local fi, err = io.open(filename, 'rb')
if not fi then return nil,err end if not fi then return nil, err end
local s = fi:read("*a") local s = fi:read("*a")
fi:close() fi:close()
return s return s
end end
-- Write data to file all at once, return true if success / false,err if failure -- Write data to file all at once, return true if success / false,err if failure
function io.writeall(filename, data) function io.writeall(filename, data)
local fi,err = io.open(filename, 'wb') local fi, err = io.open(filename, 'wb')
if not fi then return false,err end if not fi then return false, err end
fi:write(data) fi:write(data)
fi:close() fi:close()
return true,nil return true, nil
end end
-- Math -- Math
-- Round -- Round
function math.round(x) function math.round(x)
return math.floor(x+0.5) return math.floor(x + 0.5)
end end
-- Mod that accounts for floating point inaccuracy -- Mod that accounts for floating point inaccuracy
function math.mod(a,b) function math.mod(a, b)
local m = a%b local m = a % b
if m==0 or math.abs(m)<1e-15 or math.abs(m-b)<1e-15 then return 0 if m == 0 or math.abs(m) < 1e-15 or math.abs(m - b) < 1e-15 then
else return m end return 0
else
return m
end
end end
-- Clamp value between min and max -- Clamp value between min and max
function math.clamp(v, n, x) function math.clamp(v, n, x)
return math.min(x, math.max(v, n)) return math.min(x, math.max(v, n))

View File

@@ -1,26 +1,28 @@
-- Vector math class with operators -- Vector math class with operators
local vector_meta local vector_meta
local vector_new local vector_new
local function vector_check(v, n, name, argn) local function vector_check(v, n, name, argn)
if not v.__is_vector then if not v.__is_vector then
error('vector '..name..': argument #'..(argn or 1) error('vector ' .. name .. ': argument #' .. (argn or 1)
..': expected vector, got '..type(v), n+1) end .. ': expected vector, got ' .. type(v), n + 1)
end
end end
local function vector_checksamelen(v1, v2, name) local function vector_checksamelen(v1, v2, name)
vector_check(v1, 3, name, 1) vector_check(v1, 3, name, 1)
vector_check(v2, 3, name, 2) vector_check(v2, 3, name, 2)
if #v1~=#v2 then if #v1 ~= #v2 then
error('vector '..name..': vector lengths do not match (lengths are ' error('vector ' .. name .. ': vector lengths do not match (lengths are '
..#v1..' and '..#v2..')', 3) end .. #v1 .. ' and ' .. #v2 .. ')', 3)
end
return #v1 return #v1
end end
local function vector_checklen(v1, v2, name, len) local function vector_checklen(v1, v2, name, len)
vector_check(v1, 3, name, 1) vector_check(v1, 3, name, 1)
vector_check(v2, 3, name, 2) vector_check(v2, 3, name, 2)
if #v1~=len or #v2~=len then if #v1 ~= len or #v2 ~= len then
error('vector '..name..': vector lengths are not '..len..' (lengths are ' error('vector ' .. name .. ': vector lengths are not ' .. len .. ' (lengths are '
..#v1..' and '..#v2..')', 3) end .. #v1 .. ' and ' .. #v2 .. ')', 3)
end
end end
local function vector_opnnn(name, op) local function vector_opnnn(name, op)
return function(v1, v2) return function(v1, v2)
@@ -34,8 +36,8 @@ local function vector_opnnn(name, op)
end end
local function vector_opnxn(name, op) local function vector_opnxn(name, op)
return function(v1, v2) return function(v1, v2)
local v1v = type(v1)=='table' and v1.__is_vector local v1v = type(v1) == 'table' and v1.__is_vector
local v2v = type(v2)=='table' and v2.__is_vector local v2v = type(v2) == 'table' and v2.__is_vector
if v1v and v2v then if v1v and v2v then
local len = vector_checksamelen(v1, v2, name) local len = vector_checksamelen(v1, v2, name)
local v3 = {} local v3 = {}
@@ -44,7 +46,7 @@ local function vector_opnxn(name, op)
end end
return vector_new(v3) return vector_new(v3)
else else
if v2v then v1,v2 = v2,v1 end if v2v then v1, v2 = v2, v1 end
local len = #v1 local len = #v1
local v3 = {} local v3 = {}
for i = 1, len do for i = 1, len do
@@ -65,26 +67,32 @@ local function vector_opn0n(name, op)
return vector_new(v2) return vector_new(v2)
end end
end end
local vector_indices = {x = 1, y = 2, z = 3, w = 4, r = 1, g = 2, b = 3, a = 4} local vector_indices = { x = 1, y = 2, z = 3, w = 4, r = 1, g = 2, b = 3, a = 4 }
local vector_meta = { local vector_meta = {
__is_vector = true, __is_vector = true,
__index = function(t, k) __index = function(t, k)
if tonumber(k) then return rawget(t, k) if tonumber(k) then
elseif vector_indices[k] then return rawget(t, vector_indices[k]) return rawget(t, k)
else return getmetatable(t)[k] elseif vector_indices[k] then
return rawget(t, vector_indices[k])
else
return getmetatable(t)[k]
end end
end, end,
__newindex = function(t, k, v) __newindex = function(t, k, v)
if tonumber(k) then rawset(t, k, v) if tonumber(k) then
elseif vector_indices[k] then rawset(t, vector_indices[k], v) rawset(t, k, v)
else return elseif vector_indices[k] then
rawset(t, vector_indices[k], v)
else
return
end end
end, end,
__add = vector_opnnn('add', function(x1, x2) return x1+x2 end), __add = vector_opnnn('add', function(x1, x2) return x1 + x2 end),
__sub = vector_opnnn('sub', function(x1, x2) return x1-x2 end), __sub = vector_opnnn('sub', function(x1, x2) return x1 - x2 end),
__mul = vector_opnxn('mul', function(x1, x2) return x1*x2 end), __mul = vector_opnxn('mul', function(x1, x2) return x1 * x2 end),
__div = vector_opnxn('div', function(x1, x2) return x1/x2 end), __div = vector_opnxn('div', function(x1, x2) return x1 / x2 end),
__pow = vector_opnxn('pow', function(x1, x2) return x1^x2 end), __pow = vector_opnxn('pow', function(x1, x2) return x1 ^ x2 end),
__unm = vector_opn0n('inv', function(x1) return -x1 end), __unm = vector_opn0n('inv', function(x1) return -x1 end),
__concat = nil, __concat = nil,
--__len = function(v1) return #v1 end, --__len = function(v1) return #v1 end,
@@ -92,7 +100,7 @@ local vector_meta = {
__eq = function(v1, v2) __eq = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'equals') local len = vector_checksamelen(v1, v2, 'equals')
for i = 1, len do for i = 1, len do
if v1[i]~=v2[i] then return false end if v1[i] ~= v2[i] then return false end
end end
return true return true
end, end,
@@ -105,7 +113,7 @@ local vector_meta = {
local len = #v1 local len = #v1
local l = 0 local l = 0
for i = 1, len do for i = 1, len do
l = l + v1[i]^2 l = l + v1[i] ^ 2
end end
return math.sqrt(l) return math.sqrt(l)
end, end,
@@ -115,8 +123,11 @@ local vector_meta = {
local len = #v1 local len = #v1
local v3 = {} local v3 = {}
for i = 1, len do for i = 1, len do
if length==0 then v3[i] = 0 if length == 0 then
else v3[i] = v1[i]/length end v3[i] = 0
else
v3[i] = v1[i] / length
end
end end
return vector_new(v3) return vector_new(v3)
end, end,
@@ -127,48 +138,56 @@ local vector_meta = {
for i = 1, len do for i = 1, len do
table.insert(st, tostring(v1[i])) table.insert(st, tostring(v1[i]))
end end
return 'vector{ '..table.concat(st, ', ')..' }' return 'vector{ ' .. table.concat(st, ', ') .. ' }'
end, end,
unpack = function(v1) return unpack(v1) end, unpack = function(v1) return unpack(v1) end,
floor = vector_opn0n('floor', function(x1) return math.floor(x1) end), floor = vector_opn0n('floor', function(x1) return math.floor(x1) end),
ceil = vector_opn0n('ceil' , function(x1) return math.ceil (x1) end), ceil = vector_opn0n('ceil', function(x1) return math.ceil(x1) end),
round = vector_opn0n('round', function(x1) return math.floor(x1+0.5) end), round = vector_opn0n('round', function(x1) return math.floor(x1 + 0.5) end),
dot = function(v1, v2) dot = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'dot') local len = vector_checksamelen(v1, v2, 'dot')
local x = 0 local x = 0
for i = 1, len do for i = 1, len do
x = x + v1[i]*v2[i] x = x + v1[i] * v2[i]
end end
return x return x
end, end,
cross = function(v1, v2) cross = function(v1, v2)
vector_checklen(v1, v2, 'cross', 3) vector_checklen(v1, v2, 'cross', 3)
return vector_new{ return vector_new {
v1[2]*v2[3] - v1[3]*v2[2], v1[2] * v2[3] - v1[3] * v2[2],
v1[3]*v2[1] - v1[1]*v2[3], v1[3] * v2[1] - v1[1] * v2[3],
v1[1]*v2[2] - v1[2]*v2[1], v1[1] * v2[2] - v1[2] * v2[1],
} }
end, end,
rotateByAngleId = function(v1, r) rotateByAngleId = function(v1, r)
--vector_check(v1, 2, 'rotate') --vector_check(v1, 2, 'rotate')
if type(r)~='number' or r%1~=0 then if type(r) ~= 'number' or r % 1 ~= 0 then
error('vector rotateByAngleId: invalid rotation '..tostring(r), 2) end error('vector rotateByAngleId: invalid rotation ' .. tostring(r), 2)
r = r%4 end
r = r % 4
local v2 local v2
if r==0 then v2 = vector_new{ v1[1], v1[2], v1[3] } if r == 0 then
elseif r==1 then v2 = vector_new{ v1[2], -v1[1], v1[3] } v2 = vector_new { v1[1], v1[2], v1[3] }
elseif r==2 then v2 = vector_new{ -v1[1], -v1[2], v1[3] } elseif r == 1 then
elseif r==3 then v2 = vector_new{ -v1[2], v1[1], v1[3] } v2 = vector_new { v1[2], -v1[1], v1[3] }
else error('vector rotateByAngleId: invalid rotation '..r, 2) end elseif r == 2 then
v2 = vector_new { -v1[1], -v1[2], v1[3] }
elseif r == 3 then
v2 = vector_new { -v1[2], v1[1], v1[3] }
else
error('vector rotateByAngleId: invalid rotation ' .. r, 2)
end
return v2 return v2
end, end,
rotateZ = function(v, r) rotateZ = function(v, r)
--vector_check(v, 2, 'rotate2d') --vector_check(v, 2, 'rotate2d')
if type(r)~='number' then if type(r) ~= 'number' then
error('vector rotateZ: invalid rotation '..tostring(r), 2) end error('vector rotateZ: invalid rotation ' .. tostring(r), 2)
local len = math.sqrt(v[1]^2 + v[2]^2) end
local len = math.sqrt(v[1] ^ 2 + v[2] ^ 2)
local ang = math.atan2(v[2], v[1]) + r local ang = math.atan2(v[2], v[1]) + r
local v2 = vector_new{ math.cos(ang)*len, math.sin(ang)*len } local v2 = vector_new { math.cos(ang) * len, math.sin(ang) * len }
return v2 return v2
end, end,
tsString = function(v) tsString = function(v)
@@ -178,8 +197,8 @@ local vector_meta = {
distance = function(v1, v2) distance = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'distance') local len = vector_checksamelen(v1, v2, 'distance')
local sum = 0 local sum = 0
for i=1,len do for i = 1, len do
sum = sum + (v1[i] - v2[i])^2 sum = sum + (v1[i] - v2[i]) ^ 2
end end
return math.sqrt(sum) return math.sqrt(sum)
end, end,
@@ -190,21 +209,21 @@ local vector_meta = {
} }
vector_new = function(vi) vector_new = function(vi)
if vi then if vi then
if type(vi)=='string' then if type(vi) == 'string' then
local vi2 = {} local vi2 = {}
for val in vi:gmatch('[0-9%.%-e]+') do for val in vi:gmatch('[0-9%.%-e]+') do
table.insert(vi2, tonumber(val)) table.insert(vi2, tonumber(val))
end end
vi = vi2 vi = vi2
elseif type(vi)~='table' then elseif type(vi) ~= 'table' then
error('vector: argument #1: expected input table, got '..type(vi), 2) error('vector: argument #1: expected input table, got ' .. type(vi), 2)
end end
local v = {} local v = {}
if #vi>0 then if #vi > 0 then
for i = 1, #vi do v[i] = vi[i] end for i = 1, #vi do v[i] = vi[i] end
else else
for n, i in pairs(vector_indices) do v[i] = vi[n] end for n, i in pairs(vector_indices) do v[i] = vi[n] end
if #v==0 then if #v == 0 then
error('vector: argument #1: table contains no values', 2) error('vector: argument #1: table contains no values', 2)
end end
end end