1
0
forked from redo/BlockLua
This commit is contained in:
2025-10-05 19:50:29 -04:00
parent f447c039c7
commit 01f216f31e
16 changed files with 2824 additions and 2596 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,7 +102,7 @@ end
function metat.__index:send(mailt)
self:mail(mailt.from)
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)
end
else
@@ -114,7 +114,7 @@ end
function open(server, port, create)
local tp = socket.try(tp.connect(server or SERVER, port or PORT,
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
s.try = socket.newtry(function()
s:close()
@@ -125,7 +125,7 @@ end
-- convert headers to lowercase
local function lower_headers(headers)
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
end
return lower
@@ -148,7 +148,7 @@ local send_message
-- yield the headers all at once, it's faster
local function send_headers(headers)
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
end
coroutine.yield(h)
@@ -192,9 +192,13 @@ local function send_source(mesgt)
-- send body from source
while true do
local chunk, err = mesgt.body()
if err then coroutine.yield(nil, err)
elseif chunk then coroutine.yield(chunk)
else break end
if err then
coroutine.yield(nil, err)
elseif chunk then
coroutine.yield(chunk)
else
break
end
end
end
@@ -211,9 +215,13 @@ end
-- message source
function send_message(mesgt)
if base.type(mesgt.body) == "table" then send_multipart(mesgt)
elseif base.type(mesgt.body) == "function" then send_source(mesgt)
else send_string(mesgt) end
if base.type(mesgt.body) == "table" then
send_multipart(mesgt)
elseif base.type(mesgt.body) == "function" then
send_source(mesgt)
else
send_string(mesgt)
end
end
-- set defaul headers
@@ -233,8 +241,11 @@ function message(mesgt)
local co = coroutine.create(function() send_message(mesgt) end)
return function()
local ret, a, b = coroutine.resume(co)
if ret then return a, b
else return nil, a end
if ret then
return a, b
else
return nil, a
end
end
end

View File

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

View File

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

View File

@@ -4,7 +4,9 @@
-- 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
local old_io = io
@@ -50,13 +52,13 @@ local allowed_dirs_readonly = tmap {
-- so this is just a precaution.
local disallowed_exts = tmap {
-- windows
'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',
'sct','shb','shs','u3p','vb','vbe','vbs','vbscript','ws','wsf','wsh',
'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',
'sct', 'shb', 'shs', 'u3p', 'vb', 'vbe', 'vbs', 'vbscript', 'ws', 'wsf', 'wsh',
-- linux
'csh','ksh','out','run','sh',
'csh', 'ksh', 'out', 'run', 'sh',
-- 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
-- Return: clean file path if allowed (or nil if disallowed),
@@ -68,27 +70,27 @@ local function safe_path(fn, readonly)
-- whitelist characters
local ic = fn:find('[^a-zA-Z0-9_%-/ %.]')
if ic then
return nil, 'Filename \''..fn..'\' contains invalid character \''..
fn:sub(ic, ic)..'\' at position '..ic
return nil, 'Filename \'' .. fn .. '\' contains invalid character \'' ..
fn:sub(ic, ic) .. '\' at position ' .. ic
end
-- disallow up-dirs, absolute paths, and relative paths
-- './' and '../' are possible in scripts, because they're processed into
-- absolute paths in util.lua before reaching here
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
-- allow only whitelisted dirs
local dir = fn:match('^([^/]+)/')
if (not dir) or (
(not allowed_dirs[dir:lower()]) and
((not readonly) or (not allowed_dirs_readonly[dir:lower()])) ) then
return nil, 'filename is in disallowed directory '..(dir or 'nil')
((not readonly) or (not allowed_dirs_readonly[dir:lower()]))) then
return nil, 'filename is in disallowed directory ' .. (dir or 'nil')
end
-- disallow blacklisted extensions or no extension
local ext = fn:match('%.([^/%.]+)$')
if (not ext) or (disallowed_exts[ext:lower()]) then
return nil, 'Filename \''..fn..'\' has disallowed extension \''..
(ext or '')..'\''
return nil, 'Filename \'' .. fn .. '\' has disallowed extension \'' ..
(ext or '') .. '\''
end
return fn, nil
end
@@ -96,7 +98,7 @@ end
-- Wrap io.open with path sanitization
function _bllua_io_open(fn, md)
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)
if fns then
return old_io.open(fns, md)
@@ -104,6 +106,7 @@ function _bllua_io_open(fn, md)
return nil, err
end
end
-- Allow io.type (works on file handles returned by io.open)
function _bllua_io_type(f)
return old_io.type(f)
@@ -122,12 +125,13 @@ function _bllua_requiresecure(name)
name:find('^%.') or name:find('%.$') then
error('require: package name contains invalid character', 3)
elseif disallowed_packages[name] then
error('require: attempt to require disallowed module \''..name..'\'', 3)
error('require: attempt to require disallowed module \'' .. name .. '\'', 3)
else
-- todo: reimplement require to not use package.* stuff?
return old_require(name)
end
end
package = {
seeall = old_package.seeall,
}

View File

@@ -9,7 +9,7 @@ _bllua_on_unload = {}
-- Utility for getting the current filename
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)
if not info then return nil end
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
function _bllua_on_error(err)
err = err:match(': (.+)$') or err
local tracelines = {err}
local tracelines = { err }
local level = 2
while true do
local info = debug.getinfo(level)
if not info then break end
local filename = debug.getfilename(level) or info.short_src
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\'',
filename,
info.currentline==-1 and '' or info.currentline..':',
info.currentline == -1 and '' or info.currentline .. ':',
funcname
))
level = level+1
level = level + 1
end
return table.concat(tracelines, '\n')
end

View File

@@ -4,129 +4,129 @@
-- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy
bl.class('SimObject')
bl.class('ScriptObject', 'SimObject')
bl.class('SimSet', 'SimObject')
bl.class('SimGroup', 'SimSet')
bl.class('GuiControl', 'SimGroup')
bl.class('GuiTextCtrl' , 'GuiControl')
bl.class('GuiSwatchCtrl' , 'GuiControl')
bl.class('GuiButtonBaseCtrl' , 'GuiControl')
bl.class('GuiArrayCtrl' , 'GuiControl')
bl.class('GuiScrollCtrl' , 'GuiControl')
bl.class('GuiMouseEventCtrl' , 'GuiControl')
bl.class('GuiProgressCtrl' , 'GuiControl')
bl.class('GuiSliderCtrl' , 'GuiControl')
bl.class('GuiConsoleTextCtrl' , 'GuiControl')
bl.class('GuiTSCtrl' , 'GuiControl')
bl.class('GuiObjectView', 'GuiTSCtrl')
bl.class('GameTSCtrl' , 'GuiTSCtrl')
bl.class('EditTSCtrl' , 'GuiTSCtrl')
bl.class('GuiPlayerView', 'GuiTSCtrl')
bl.class('GuiShapeNameHud' , 'GuiControl')
bl.class('GuiHealthBarHud' , 'GuiControl')
bl.class('GuiGraphCtrl' , 'GuiControl')
bl.class('GuiInspector' , 'GuiControl')
bl.class('GuiChunkedBitmapCtrl', 'GuiControl')
bl.class('GuiInputCtrl' , 'GuiControl')
bl.class('GuiNoMouseCtrl' , 'GuiControl')
bl.class('GuiBitmapBorderCtrl' , 'GuiControl')
bl.class('GuiBackgroundCtrl' , 'GuiControl')
bl.class('GuiEditorRuler' , 'GuiControl')
bl.class('GuiClockHud' , 'GuiControl')
bl.class('GuiEditCtrl' , 'GuiControl')
bl.class('GuiFilterCtrl' , 'GuiControl')
bl.class('GuiFrameSetCtrl' , 'GuiControl')
bl.class('GuiMenuBar' , 'GuiControl')
bl.class('GuiMessageVectorCtrl', 'GuiControl')
bl.class('GuiBitmapCtrl' , 'GuiControl')
bl.class('GuiCrossHairHud', 'GuiBitmapCtrl')
bl.class('ScriptGroup', 'SimGroup')
bl.class('NetConnection', 'SimGroup')
bl.class('GameConnection', 'NetConnection')
bl.class('Path', 'SimGroup')
bl.class('TCPObject', 'SimObject')
bl.class('SOCKObject', 'TCPObject')
bl.class('HTTPObject', 'TCPObject')
bl.class('SimDataBlock', 'SimObject')
bl.class('AudioEnvironment' , 'SimDataBlock')
bl.class('AudioSampleEnvironment', 'SimDataBlock')
bl.class('AudioDescription' , 'SimDataBlock')
bl.class('GameBaseData' , 'SimDataBlock')
bl.class('ShapeBaseData' , 'GameBaseData')
bl.class('CameraData' , 'ShapeBaseData')
bl.class('ItemData' , 'ShapeBaseData')
bl.class('MissionMarkerData', 'ShapeBaseData')
bl.class('PathCameraData' , 'ShapeBaseData')
bl.class('PlayerData' , 'ShapeBaseData')
bl.class('StaticShapeData' , 'ShapeBaseData')
bl.class('VehicleData' , 'ShapeBaseData')
bl.class('FlyingVehicleData' , 'VehicleData')
bl.class('WheeledVehicleData', 'VehicleData')
bl.class('DebrisData' , 'GameBaseData')
bl.class('ProjectileData' , 'GameBaseData')
bl.class('ShapeBaseImageData' , 'GameBaseData')
bl.class('TriggerData' , 'GameBaseData')
bl.class('ExplosionData' , 'GameBaseData')
bl.class('fxLightData' , 'GameBaseData')
bl.class('LightningData' , 'GameBaseData')
bl.class('ParticleEmitterNodeData', 'GameBaseData')
bl.class('SplashData' , 'GameBaseData')
bl.class('fxDTSBrickData' , 'GameBaseData')
bl.class('ParticleEmitterData' , 'GameBaseData')
bl.class('WheeledVehicleTire' , 'SimDataBlock')
bl.class('WheeledVehicleSpring' , 'SimDataBlock')
bl.class('TSShapeConstructor' , 'SimDataBlock')
bl.class('AudioProfile' , 'SimDataBlock')
bl.class('ParticleData' , 'SimDataBlock')
bl.class('MaterialPropertyMap', 'SimObject')
bl.class('NetObject', 'SimObject')
bl.class('SceneObject', 'NetObject')
bl.class('GameBase', 'SceneObject')
bl.class('ShapeBase', 'GameBase')
bl.class('MissionMarker', 'ShapeBase')
bl.class('SpawnSphere' , 'MissionMarker')
bl.class('VehicleSpawnMarker', 'MissionMarker')
bl.class('Waypoint' , 'MissionMarker')
bl.class('StaticShape' , 'ShapeBase')
bl.class('ScopeAlwaysShape', 'StaticShape')
bl.class('Player' , 'ShapeBase')
bl.class('AIPlayer', 'Player')
bl.class('Camera' , 'ShapeBase')
bl.class('Item' , 'ShapeBase')
bl.class('PathCamera' , 'ShapeBase')
bl.class('Vehicle' , 'ShapeBase')
bl.class('FlyingVehicle' , 'Vehicle')
bl.class('WheeledVehicle', 'Vehicle')
bl.class('Explosion' , 'GameBase')
bl.class('Splash' , 'GameBase')
bl.class('Debris' , 'GameBase')
bl.class('Projectile' , 'GameBase')
bl.class('Trigger' , 'GameBase')
bl.class('fxLight' , 'GameBase')
bl.class('Lightning' , 'GameBase')
bl.class('ParticleEmitterNode', 'GameBase')
bl.class('ParticleEmitter' , 'GameBase')
bl.class('Precipitation' , 'GameBase')
bl.class('TSStatic' , 'SceneObject')
bl.class('VehicleBlocker', 'SceneObject')
bl.class('Marker' , 'SceneObject')
bl.class('AudioEmitter' , 'SceneObject')
bl.class('PhysicalZone' , 'SceneObject')
bl.class('fxDayCycle' , 'SceneObject')
bl.class('fxDTSBrick' , 'SceneObject')
bl.class('fxPlane' , 'SceneObject')
bl.class('fxSunLight' , 'SceneObject')
bl.class('Sky' , 'SceneObject')
bl.class('SceneRoot' , 'SceneObject')
bl.class('Sun', 'NetObject')
bl.class('GuiCursor', 'SimObject')
bl.class('ConsoleLogger' , 'SimObject')
bl.class('QuotaObject' , 'SimObject')
bl.class('FileObject' , 'SimObject')
bl.class('BanList' , 'SimObject')
bl.class('GuiControlProfile', 'SimObject')
bl.class('MessageVector' , 'SimObject')
bl.class('ActionMap' , 'SimObject')
bl.class('ScriptObject', 'SimObject')
bl.class('SimSet', 'SimObject')
bl.class('SimGroup', 'SimSet')
bl.class('GuiControl', 'SimGroup')
bl.class('GuiTextCtrl', 'GuiControl')
bl.class('GuiSwatchCtrl', 'GuiControl')
bl.class('GuiButtonBaseCtrl', 'GuiControl')
bl.class('GuiArrayCtrl', 'GuiControl')
bl.class('GuiScrollCtrl', 'GuiControl')
bl.class('GuiMouseEventCtrl', 'GuiControl')
bl.class('GuiProgressCtrl', 'GuiControl')
bl.class('GuiSliderCtrl', 'GuiControl')
bl.class('GuiConsoleTextCtrl', 'GuiControl')
bl.class('GuiTSCtrl', 'GuiControl')
bl.class('GuiObjectView', 'GuiTSCtrl')
bl.class('GameTSCtrl', 'GuiTSCtrl')
bl.class('EditTSCtrl', 'GuiTSCtrl')
bl.class('GuiPlayerView', 'GuiTSCtrl')
bl.class('GuiShapeNameHud', 'GuiControl')
bl.class('GuiHealthBarHud', 'GuiControl')
bl.class('GuiGraphCtrl', 'GuiControl')
bl.class('GuiInspector', 'GuiControl')
bl.class('GuiChunkedBitmapCtrl', 'GuiControl')
bl.class('GuiInputCtrl', 'GuiControl')
bl.class('GuiNoMouseCtrl', 'GuiControl')
bl.class('GuiBitmapBorderCtrl', 'GuiControl')
bl.class('GuiBackgroundCtrl', 'GuiControl')
bl.class('GuiEditorRuler', 'GuiControl')
bl.class('GuiClockHud', 'GuiControl')
bl.class('GuiEditCtrl', 'GuiControl')
bl.class('GuiFilterCtrl', 'GuiControl')
bl.class('GuiFrameSetCtrl', 'GuiControl')
bl.class('GuiMenuBar', 'GuiControl')
bl.class('GuiMessageVectorCtrl', 'GuiControl')
bl.class('GuiBitmapCtrl', 'GuiControl')
bl.class('GuiCrossHairHud', 'GuiBitmapCtrl')
bl.class('ScriptGroup', 'SimGroup')
bl.class('NetConnection', 'SimGroup')
bl.class('GameConnection', 'NetConnection')
bl.class('Path', 'SimGroup')
bl.class('TCPObject', 'SimObject')
bl.class('SOCKObject', 'TCPObject')
bl.class('HTTPObject', 'TCPObject')
bl.class('SimDataBlock', 'SimObject')
bl.class('AudioEnvironment', 'SimDataBlock')
bl.class('AudioSampleEnvironment', 'SimDataBlock')
bl.class('AudioDescription', 'SimDataBlock')
bl.class('GameBaseData', 'SimDataBlock')
bl.class('ShapeBaseData', 'GameBaseData')
bl.class('CameraData', 'ShapeBaseData')
bl.class('ItemData', 'ShapeBaseData')
bl.class('MissionMarkerData', 'ShapeBaseData')
bl.class('PathCameraData', 'ShapeBaseData')
bl.class('PlayerData', 'ShapeBaseData')
bl.class('StaticShapeData', 'ShapeBaseData')
bl.class('VehicleData', 'ShapeBaseData')
bl.class('FlyingVehicleData', 'VehicleData')
bl.class('WheeledVehicleData', 'VehicleData')
bl.class('DebrisData', 'GameBaseData')
bl.class('ProjectileData', 'GameBaseData')
bl.class('ShapeBaseImageData', 'GameBaseData')
bl.class('TriggerData', 'GameBaseData')
bl.class('ExplosionData', 'GameBaseData')
bl.class('fxLightData', 'GameBaseData')
bl.class('LightningData', 'GameBaseData')
bl.class('ParticleEmitterNodeData', 'GameBaseData')
bl.class('SplashData', 'GameBaseData')
bl.class('fxDTSBrickData', 'GameBaseData')
bl.class('ParticleEmitterData', 'GameBaseData')
bl.class('WheeledVehicleTire', 'SimDataBlock')
bl.class('WheeledVehicleSpring', 'SimDataBlock')
bl.class('TSShapeConstructor', 'SimDataBlock')
bl.class('AudioProfile', 'SimDataBlock')
bl.class('ParticleData', 'SimDataBlock')
bl.class('MaterialPropertyMap', 'SimObject')
bl.class('NetObject', 'SimObject')
bl.class('SceneObject', 'NetObject')
bl.class('GameBase', 'SceneObject')
bl.class('ShapeBase', 'GameBase')
bl.class('MissionMarker', 'ShapeBase')
bl.class('SpawnSphere', 'MissionMarker')
bl.class('VehicleSpawnMarker', 'MissionMarker')
bl.class('Waypoint', 'MissionMarker')
bl.class('StaticShape', 'ShapeBase')
bl.class('ScopeAlwaysShape', 'StaticShape')
bl.class('Player', 'ShapeBase')
bl.class('AIPlayer', 'Player')
bl.class('Camera', 'ShapeBase')
bl.class('Item', 'ShapeBase')
bl.class('PathCamera', 'ShapeBase')
bl.class('Vehicle', 'ShapeBase')
bl.class('FlyingVehicle', 'Vehicle')
bl.class('WheeledVehicle', 'Vehicle')
bl.class('Explosion', 'GameBase')
bl.class('Splash', 'GameBase')
bl.class('Debris', 'GameBase')
bl.class('Projectile', 'GameBase')
bl.class('Trigger', 'GameBase')
bl.class('fxLight', 'GameBase')
bl.class('Lightning', 'GameBase')
bl.class('ParticleEmitterNode', 'GameBase')
bl.class('ParticleEmitter', 'GameBase')
bl.class('Precipitation', 'GameBase')
bl.class('TSStatic', 'SceneObject')
bl.class('VehicleBlocker', 'SceneObject')
bl.class('Marker', 'SceneObject')
bl.class('AudioEmitter', 'SceneObject')
bl.class('PhysicalZone', 'SceneObject')
bl.class('fxDayCycle', 'SceneObject')
bl.class('fxDTSBrick', 'SceneObject')
bl.class('fxPlane', 'SceneObject')
bl.class('fxSunLight', 'SceneObject')
bl.class('Sky', 'SceneObject')
bl.class('SceneRoot', 'SceneObject')
bl.class('Sun', 'NetObject')
bl.class('GuiCursor', 'SimObject')
bl.class('ConsoleLogger', 'SimObject')
bl.class('QuotaObject', 'SimObject')
bl.class('FileObject', 'SimObject')
bl.class('BanList', 'SimObject')
bl.class('GuiControlProfile', 'SimObject')
bl.class('MessageVector', 'SimObject')
bl.class('ActionMap', 'SimObject')
-- Auto-generated from game scripts
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
-- 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.
@@ -8,9 +7,10 @@ ts = _bllua_ts
-- Provide limited OS functions
os = os or {}
---@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
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
-- 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 = {
read = function(file, mode)
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 mode=='*n' then
if mode == '*n' then
local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos)
if n then
file.pos = file.pos + #ws + #n
@@ -30,15 +30,15 @@ local file_meta = {
else
return nil
end
elseif mode=='*a' then
elseif mode == '*a' then
local d = file.data:sub(file.pos, #file.data)
file.pos = #file.data + 1
return d
elseif mode=='*l' then
elseif mode == '*l' then
local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos)
if not l then
l = file.data:match('^([^\r\n]*)$', file.pos); ws = '';
if l=='' then return nil end
if l == '' then return nil end
end
if l then
file.pos = file.pos + #l + #ws
@@ -46,12 +46,12 @@ local file_meta = {
else
return nil
end
elseif type(mode)=='number' then
local d = file.data:sub(file.pos, file.pos+mode)
elseif type(mode) == 'number' then
local d = file.data:sub(file.pos, file.pos + mode)
file.pos = file.pos + #d
return d
else
error('File:read: Invalid mode \''..mode..'\'', 2)
error('File:read: Invalid mode \'' .. mode .. '\'', 2)
end
end,
lines = function(file)
@@ -84,8 +84,10 @@ local function new_file_obj(fn)
return file
end
local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end
local allowed_zip_dirs = tflip{
local function tflip(t)
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'
}
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'
if not exist then return nil, err end
if mode~=nil and mode~='r' and mode~='rb' then
return nil, 'Files in zips can only be opened in read mode' end
if mode ~= nil and mode ~= 'r' and mode ~= 'rb' then
return nil, 'Files in zips can only be opened in read mode'
end
-- return a temp lua file object with the data
local fi = new_file_obj(fn)
@@ -117,7 +120,7 @@ function io.open(fn, mode, errn)
if curfn == '' then curfn = nil end
if fn:find('^%.') then
local relfn = curfn and fn:find('^%./') and
curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '')
curfn:gsub('[^/]+$', '') .. fn:gsub('^%./', '')
if relfn then
local fi, err = io_open_absolute(relfn, mode)
return fi, err, relfn
@@ -129,17 +132,19 @@ function io.open(fn, mode, errn)
return fi, err, fn
end
end
---@diagnostic disable-next-line: duplicate-set-field
function io.lines(fn)
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()
end
---@diagnostic disable-next-line: duplicate-set-field
function io.type(f)
---@diagnostic disable-next-line: undefined-field
if type(f)=='table' and f._is_file then
---@diagnostic disable-next-line: undefined-field
---@diagnostic disable-next-line: undefined-field
if type(f) == 'table' and f._is_file then
---@diagnostic disable-next-line: undefined-field
return f._is_open and 'file' or 'closed file'
else
return _bllua_io_type(f)
@@ -150,13 +155,13 @@ end
function dofile(fn, errn)
errn = errn or 1
local fi, err, fn2 = io.open(fn, 'r', errn+1)
if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end
local fi, err, fn2 = io.open(fn, 'r', errn + 1)
if not fi then error('Error executing file \'' .. fn2 .. '\': ' .. err, errn + 1) end
print('Executing '..fn2)
print('Executing ' .. fn2)
local text = fi:read('*a')
fi:close()
return assert(loadstring('--[['..fn2..']]'..text))()
return assert(loadstring('--[[' .. fn2 .. ']]' .. text))()
end
-- provide require (just a wrapper for dofile)
@@ -165,7 +170,7 @@ end
-- blockland directory
-- current add-on
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
fi:close()
return fn2
@@ -178,20 +183,20 @@ function require(mod)
if require_memo[mod] then return unpack(require_memo[mod]) end
local fp = mod:gsub('%.', '/')
local fns = {
'./'..fp..'.lua', -- local file
'./'..fp..'/init.lua', -- local library
fp..'.lua', -- global file
fp..'/init.lua', -- global library
'./' .. fp .. '.lua', -- local file
'./' .. fp .. '/init.lua', -- local library
fp .. '.lua', -- global file
fp .. '/init.lua', -- global library
}
if fp:lower():find('^add-ons/') then
local addonpath = fp:lower():match('^add-ons/[^/]+')..'/'
table.insert(fns, addonpath..fp..'.lua') -- add-on file
table.insert(fns, addonpath..fp..'/init.lua') -- add-on library
local addonpath = fp:lower():match('^add-ons/[^/]+') .. '/'
table.insert(fns, addonpath .. fp .. '.lua') -- add-on file
table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library
end
for _,fn in ipairs(fns) do
for _, fn in ipairs(fns) do
local fne = file_exists(fn, 2)
if fne then
local res = {dofile(fne, 2)}
local res = { dofile(fne, 2) }
require_memo[mod] = res
return unpack(res)
end
@@ -201,18 +206,22 @@ end
-- Exposure to TS
function _bllua_getvar(name) return _G[name] end
function _bllua_setvar(name, val) _G[name] = val end
function _bllua_eval(code) return loadstring(code)() end
function _bllua_exec(fn) return dofile(fn, 2) end
local function isValidCode(code)
local f,e = loadstring(code)
return f~=nil
local f, e = loadstring(code)
return f ~= nil
end
function _bllua_smarteval(code)
if (not code:find('^print%(')) and isValidCode('print('..code..')') then
code = 'print('..code..')' end
local f,e = loadstring(code)
if (not code:find('^print%(')) and isValidCode('print(' .. code .. ')') then
code = 'print(' .. code .. ')'
end
local f, e = loadstring(code)
if f then
return f()
else

View File

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

View File

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