Compare commits

...

10 Commits

28 changed files with 1491 additions and 2918 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,13 +0,0 @@
@echo off
cd /d %~dp0
set buildargs=-Wall -Werror -m32 -shared -Isrc -Iinc/tsfuncs -Iinc/lua -lpsapi -L. -llua5.1 -static-libgcc -static-libstdc++
echo on
g++ src/bllua4.cpp %buildargs% -o BlockLua.dll && g++ -DBLLUA_UNSAFE src/bllua4.cpp %buildargs% -o BlockLua-Unsafe.dll
@echo off
rem objdump -d BlockLua.dll > BlockLua.dll.dump.txt
rem objdump -d BlockLua-Unsafe.dll > BlockLua-Unsafe.dll.dump.txt
pause

Binary file not shown.

Binary file not shown.

View File

@@ -1,292 +0,0 @@
-----------------------------------------------------------------------------
-- LTN12 - Filters, sources, sinks and pumps.
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: ltn12.lua,v 1.31 2006/04/03 04:45:42 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module
-----------------------------------------------------------------------------
local string = require("string")
local table = require("table")
local base = _G
module("ltn12")
filter = {}
source = {}
sink = {}
pump = {}
-- 2048 seems to be better in windows...
BLOCKSIZE = 2048
_VERSION = "LTN12 1.0.1"
-----------------------------------------------------------------------------
-- Filter stuff
-----------------------------------------------------------------------------
-- returns a high level filter that cycles a low-level filter
function filter.cycle(low, ctx, extra)
base.assert(low)
return function(chunk)
local ret
ret, ctx = low(ctx, chunk, extra)
return ret
end
end
-- chains a bunch of filters together
-- (thanks to Wim Couwenberg)
function filter.chain(...)
local n = table.getn(arg)
local top, index = 1, 1
local retry = ""
return function(chunk)
retry = chunk and retry
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
else
top = top+1
index = top
end
else
chunk = arg[index](chunk or "")
if chunk == "" then
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
end
end
end
end
-----------------------------------------------------------------------------
-- Source stuff
-----------------------------------------------------------------------------
-- create an empty source
local function empty()
return nil
end
function source.empty()
return empty
end
-- returns a source that just outputs an error
function source.error(err)
return function()
return nil, err
end
end
-- creates a file source
function source.file(handle, io_err)
if handle then
return function()
local chunk = handle:read(BLOCKSIZE)
if not chunk then handle:close() end
return chunk
end
else return source.error(io_err or "unable to open file") end
end
-- turns a fancy source into a simple source
function source.simplify(src)
base.assert(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
end
end
-- creates string source
function source.string(s)
if s then
local i = 1
return function()
local chunk = string.sub(s, i, i+BLOCKSIZE-1)
i = i + BLOCKSIZE
if chunk ~= "" then return chunk
else return nil end
end
else return source.empty() end
end
-- creates rewindable source
function source.rewind(src)
base.assert(src)
local t = {}
return function(chunk)
if not chunk then
chunk = table.remove(t)
if not chunk then return src()
else return chunk end
else
table.insert(t, chunk)
end
end
end
function source.chain(src, f)
base.assert(src and f)
local last_in, last_out = "", ""
local state = "feeding"
local err
return function()
if not last_out then
base.error('source is empty!', 2)
end
while true do
if state == "feeding" then
last_in, err = src()
if err then return nil, err end
last_out = f(last_in)
if not last_out then
if last_in then
base.error('filter returned inappropriate nil')
else
return nil
end
elseif last_out ~= "" then
state = "eating"
if last_in then last_in = "" end
return last_out
end
else
last_out = f(last_in)
if last_out == "" then
if last_in == "" then
state = "feeding"
else
base.error('filter returned ""')
end
elseif not last_out then
if last_in then
base.error('filter returned inappropriate nil')
else
return nil
end
else
return last_out
end
end
end
end
end
-- creates a source that produces contents of several sources, one after the
-- other, as if they were concatenated
-- (thanks to Wim Couwenberg)
function source.cat(...)
local src = table.remove(arg, 1)
return function()
while src do
local chunk, err = src()
if chunk then return chunk end
if err then return nil, err end
src = table.remove(arg, 1)
end
end
end
-----------------------------------------------------------------------------
-- Sink stuff
-----------------------------------------------------------------------------
-- creates a sink that stores into a table
function sink.table(t)
t = t or {}
local f = function(chunk, err)
if chunk then table.insert(t, chunk) end
return 1
end
return f, t
end
-- turns a fancy sink into a simple sink
function sink.simplify(snk)
base.assert(snk)
return function(chunk, err)
local ret, err_or_new = snk(chunk, err)
if not ret then return nil, err_or_new end
snk = err_or_new or snk
return 1
end
end
-- creates a file sink
function sink.file(handle, io_err)
if handle then
return function(chunk, err)
if not chunk then
handle:close()
return 1
else return handle:write(chunk) end
end
else return sink.error(io_err or "unable to open file") end
end
-- creates a sink that discards data
local function null()
return 1
end
function sink.null()
return null
end
-- creates a sink that just returns an error
function sink.error(err)
return function()
return nil, err
end
end
-- chains a sink with a filter
function sink.chain(f, snk)
base.assert(f and snk)
return function(chunk, err)
if chunk ~= "" then
local filtered = f(chunk)
local done = chunk and ""
while true do
local ret, snkerr = snk(filtered, err)
if not ret then return nil, snkerr end
if filtered == done then return 1 end
filtered = f(done)
end
else return 1 end
end
end
-----------------------------------------------------------------------------
-- Pump stuff
-----------------------------------------------------------------------------
-- pumps one chunk from the source to the sink
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
end
-- pumps all data from a source to a sink, using a step function
function pump.all(src, snk, step)
base.assert(src and snk)
step = step or pump.step
while true do
local ret, err = step(src, snk)
if not ret then
if err then return nil, err
else return 1 end
end
end
end

View File

@@ -1,87 +0,0 @@
-----------------------------------------------------------------------------
-- MIME support for the Lua language.
-- Author: Diego Nehab
-- Conforming to RFCs 2045-2049
-- RCS ID: $Id: mime.lua,v 1.29 2007/06/11 23:44:54 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local ltn12 = require("ltn12")
local mime = require("mime.core")
--local io = require("io")
local string = require("string")
module("mime")
-- encode, decode and wrap algorithm tables
encodet = {}
decodet = {}
wrapt = {}
-- creates a function that chooses a filter by name from a given table
local function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
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
end
end
-- define the encoding filters
encodet['base64'] = function()
return ltn12.filter.cycle(b64, "")
end
encodet['quoted-printable'] = function(mode)
return ltn12.filter.cycle(qp, "",
(mode == "binary") and "=0D=0A" or "\r\n")
end
-- define the decoding filters
decodet['base64'] = function()
return ltn12.filter.cycle(unb64, "")
end
decodet['quoted-printable'] = function()
return ltn12.filter.cycle(unqp, "")
end
local function format(chunk)
if chunk then
if chunk == "" then return "''"
else return string.len(chunk) end
else return "nil" end
end
-- define the line-wrap filters
wrapt['text'] = function(length)
length = length or 76
return ltn12.filter.cycle(wrp, length, length)
end
wrapt['base64'] = wrapt['text']
wrapt['default'] = wrapt['text']
wrapt['quoted-printable'] = function()
return ltn12.filter.cycle(qpwrp, 76, 76)
end
-- function that choose the encoding, decoding or wrap algorithm
encode = choose(encodet)
decode = choose(decodet)
wrap = choose(wrapt)
-- define the end-of-line normalization filter
function normalize(marker)
return ltn12.filter.cycle(eol, 0, marker)
end
-- high level stuffing filter
function stuff()
return ltn12.filter.cycle(dot, 2)
end

Binary file not shown.

View File

@@ -1,133 +0,0 @@
-----------------------------------------------------------------------------
-- LuaSocket helper module
-- Author: Diego Nehab
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket.core")
module("socket")
-----------------------------------------------------------------------------
-- Exported auxiliar functions
-----------------------------------------------------------------------------
function connect(address, port, laddress, lport)
local sock, err = socket.tcp()
if not sock then return nil, err end
if laddress then
local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end
function bind(host, port, backlog)
local sock, err = socket.tcp()
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port)
if not res then return nil, err end
res, err = sock:listen(backlog)
if not res then return nil, err end
return sock
end
try = newtry()
function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
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
end
end
-----------------------------------------------------------------------------
-- Socket sources and sinks, conforming to LTN12
-----------------------------------------------------------------------------
-- create namespaces inside LuaSocket namespace
sourcet = {}
sinkt = {}
BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then
sock:close()
return 1
else return sock:send(chunk) end
end
})
end
sinkt["keep-open"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if chunk then return sock:send(chunk)
else return 1 end
end
})
end
sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt)
sourcet["by-length"] = function(sock, length)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size)
if err then return nil, err end
length = length - string.len(chunk)
return chunk
end
})
end
sourcet["until-closed"] = function(sock)
local done
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk
elseif err == "closed" then
sock:close()
done = 1
return partial
else return nil, err end
end
})
end
sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet)

Binary file not shown.

View File

@@ -1,281 +0,0 @@
-----------------------------------------------------------------------------
-- FTP support for the Lua language
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: ftp.lua,v 1.45 2007/07/11 19:25:47 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local table = require("table")
local string = require("string")
local math = require("math")
local socket = require("socket")
local url = require("socket.url")
local tp = require("socket.tp")
local ltn12 = require("ltn12")
module("socket.ftp")
-----------------------------------------------------------------------------
-- Program constants
-----------------------------------------------------------------------------
-- timeout in seconds before the program gives up on a connection
TIMEOUT = 60
-- default port for ftp service
PORT = 21
-- this is the default anonymous password. used when no password is
-- provided in url. should be changed to your e-mail.
USER = "ftp"
PASSWORD = "anonymous@anonymous.org"
-----------------------------------------------------------------------------
-- Low level FTP API
-----------------------------------------------------------------------------
local metat = { __index = {} }
function open(server, port, create)
local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create))
local f = base.setmetatable({ tp = tp }, metat)
-- make sure everything gets closed in an exception
f.try = socket.newtry(function() f:close() end)
return f
end
function metat.__index:portconnect()
self.try(self.server:settimeout(TIMEOUT))
self.data = self.try(self.server:accept())
self.try(self.data:settimeout(TIMEOUT))
end
function metat.__index:pasvconnect()
self.data = self.try(socket.tcp())
self.try(self.data:settimeout(TIMEOUT))
self.try(self.data:connect(self.pasvt.ip, self.pasvt.port))
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})
if code == 331 then
self.try(self.tp:command("pass", password or PASSWORD))
self.try(self.tp:check("2.."))
end
return 1
end
function metat.__index:pasv()
self.try(self.tp:command("pasv"))
local code, reply = self.try(self.tp:check("2.."))
local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)"
local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern))
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
}
if self.server then
self.server:close()
self.server = nil
end
return self.pasvt.ip, self.pasvt.port
end
function metat.__index:port(ip, port)
self.pasvt = nil
if not ip then
ip, port = self.try(self.tp:getcontrol():getsockname())
self.server = self.try(socket.bind(ip, 0))
ip, port = self.try(self.server:getsockname())
self.try(self.server:settimeout(TIMEOUT))
end
local pl = math.mod(port, 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.."))
return 1
end
function metat.__index:send(sendt)
self.try(self.pasvt or self.server, "need port or pasv first")
-- if there is a pasvt table, we already sent a PASV command
-- we just get the data connection into self.data
if self.pasvt then self:pasvconnect() end
-- get the transfer argument and command
local argument = sendt.argument or
url.unescape(string.gsub(sendt.path or "", "^[/\\]", ""))
if argument == "" then argument = nil end
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.."})
-- 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 checkstep = function(src, snk)
-- check status in control connection while downloading
local readyt = socket.select(readt, nil, 0)
if readyt[tp] then code = self.try(self.tp:check("2..")) end
return step(src, snk)
end
local sink = socket.sink("close-when-done", self.data)
-- transfer all data and check error
self.try(ltn12.pump.all(sendt.source, sink, checkstep))
if string.find(code, "1..") then self.try(self.tp:check("2..")) end
-- done with data connection
self.data:close()
-- find out how many bytes were sent
local sent = socket.skip(1, self.data:getstats())
self.data = nil
return sent
end
function metat.__index:receive(recvt)
self.try(self.pasvt or self.server, "need port or pasv first")
if self.pasvt then self:pasvconnect() end
local argument = recvt.argument or
url.unescape(string.gsub(recvt.path or "", "^[/\\]", ""))
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.."})
if not self.pasvt then self:portconnect() end
local source = socket.source("until-closed", self.data)
local step = recvt.step or ltn12.pump.step
self.try(ltn12.pump.all(source, recvt.sink, step))
if string.find(code, "1..") then self.try(self.tp:check("2..")) end
self.data:close()
self.data = nil
return 1
end
function metat.__index:cwd(dir)
self.try(self.tp:command("cwd", dir))
self.try(self.tp:check(250))
return 1
end
function metat.__index:type(type)
self.try(self.tp:command("type", type))
self.try(self.tp:check(200))
return 1
end
function metat.__index:greet()
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
function metat.__index:quit()
self.try(self.tp:command("quit"))
self.try(self.tp:check("2.."))
return 1
end
function metat.__index:close()
if self.data then self.data:close() end
if self.server then self.server:close() end
return self.tp:close()
end
-----------------------------------------------------------------------------
-- High level FTP API
-----------------------------------------------------------------------------
local function override(t)
if t.url then
local u = url.parse(t.url)
for i,v in base.pairs(t) do
u[i] = v
end
return u
else return t end
end
local function tput(putt)
putt = override(putt)
socket.try(putt.host, "missing hostname")
local f = open(putt.host, putt.port, putt.create)
f:greet()
f:login(putt.user, putt.password)
if putt.type then f:type(putt.type) end
f:pasv()
local sent = f:send(putt)
f:quit()
f:close()
return sent
end
local default = {
path = "/",
scheme = "ftp"
}
local function parse(u)
local t = socket.try(url.parse(u, default))
socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'")
socket.try(t.host, "missing hostname")
local pat = "^type=(.)$"
if t.params then
t.type = socket.skip(2, string.find(t.params, pat))
socket.try(t.type == "a" or t.type == "i",
"invalid type '" .. t.type .. "'")
end
return t
end
local function sput(u, body)
local putt = parse(u)
putt.source = ltn12.source.string(body)
return tput(putt)
end
put = socket.protect(function(putt, body)
if base.type(putt) == "string" then return sput(putt, body)
else return tput(putt) end
end)
local function tget(gett)
gett = override(gett)
socket.try(gett.host, "missing hostname")
local f = open(gett.host, gett.port, gett.create)
f:greet()
f:login(gett.user, gett.password)
if gett.type then f:type(gett.type) end
f:pasv()
f:receive(gett)
f:quit()
return f:close()
end
local function sget(u)
local gett = parse(u)
local t = {}
gett.sink = ltn12.sink.table(t)
tget(gett)
return table.concat(t)
end
command = socket.protect(function(cmdt)
cmdt = override(cmdt)
socket.try(cmdt.host, "missing hostname")
socket.try(cmdt.command, "missing command")
local f = open(cmdt.host, cmdt.port, cmdt.create)
f:greet()
f:login(cmdt.user, cmdt.password)
f.try(f.tp:command(cmdt.command, cmdt.argument))
if cmdt.check then f.try(f.tp:check(cmdt.check)) end
f:quit()
return f:close()
end)
get = socket.protect(function(gett)
if base.type(gett) == "string" then return sget(gett)
else return tget(gett) end
end)

View File

@@ -1,350 +0,0 @@
-----------------------------------------------------------------------------
-- HTTP/1.1 client support for the Lua language.
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: http.lua,v 1.70 2007/03/12 04:08:40 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-------------------------------------------------------------------------------
local socket = require("socket")
local url = require("socket.url")
local ltn12 = require("ltn12")
local mime = require("mime")
local string = require("string")
local base = _G
local table = require("table")
module("socket.http")
-----------------------------------------------------------------------------
-- Program constants
-----------------------------------------------------------------------------
-- connection timeout in seconds
TIMEOUT = 60
-- default port for document retrieval
PORT = 80
-- user agent field sent in request
USERAGENT = socket._VERSION
-----------------------------------------------------------------------------
-- Reads MIME headers from a connection, unfolding where needed
-----------------------------------------------------------------------------
local function receiveheaders(sock, headers)
local line, name, value, err
headers = headers or {}
-- get first line
line, err = sock:receive()
if err then return nil, err end
-- headers go until a blank line is found
while line ~= "" do
-- get field-name and value
name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)"))
if not (name and value) then return nil, "malformed reponse headers" end
name = string.lower(name)
-- get next line (value might be folded)
line, err = sock:receive()
if err then return nil, err end
-- unfold any folded values
while string.find(line, "^%s") do
value = value .. line
line = sock:receive()
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
end
return headers
end
-----------------------------------------------------------------------------
-- Extra sources and sinks
-----------------------------------------------------------------------------
socket.sourcet["http-chunked"] = function(sock, headers)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
-- get chunk size, skip extention
local line, err = sock:receive()
if err then return nil, err end
local size = base.tonumber(string.gsub(line, ";.*", ""), 16)
if not size then return nil, "invalid chunk size" end
-- was it the last chunk?
if size > 0 then
-- if not, get chunk and skip terminating CRLF
local chunk, err, part = sock:receive(size)
if chunk then sock:receive() end
return chunk, err
else
-- if it was, read trailers into headers table
headers, err = receiveheaders(sock, headers)
if not headers then return nil, err end
end
end
})
end
socket.sinkt["http-chunked"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then return sock:send("0\r\n\r\n") end
local size = string.format("%X\r\n", string.len(chunk))
return sock:send(size .. chunk .. "\r\n")
end
})
end
-----------------------------------------------------------------------------
-- Low level HTTP API
-----------------------------------------------------------------------------
local metat = { __index = {} }
function open(host, port, create)
-- create socket with user connect function, or with default
local c = socket.try((create or socket.tcp)())
local h = base.setmetatable({ c = c }, metat)
-- create finalized try
h.try = socket.newtry(function() h:close() end)
-- set timeout before connecting
h.try(c:settimeout(TIMEOUT))
h.try(c:connect(host, port or PORT))
-- here everything worked
return h
end
function metat.__index:sendrequestline(method, uri)
local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri)
return self.try(self.c:send(reqline))
end
function metat.__index:sendheaders(headers)
local h = "\r\n"
for i, v in base.pairs(headers) do
h = i .. ": " .. v .. "\r\n" .. h
end
self.try(self.c:send(h))
return 1
end
function metat.__index:sendbody(headers, source, step)
source = source or ltn12.source.empty()
step = step or ltn12.pump.step
-- if we don't know the size in advance, send chunked and hope for the best
local mode = "http-chunked"
if headers["content-length"] then mode = "keep-open" end
return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step))
end
function metat.__index:receivestatusline()
local status = self.try(self.c:receive(5))
-- identify HTTP/0.9 responses, which do not contain a status line
-- this is just a heuristic, but is what the RFC recommends
if status ~= "HTTP/" then return nil, status end
-- otherwise proceed reading a status line
status = self.try(self.c:receive("*l", status))
local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)"))
return self.try(base.tonumber(code), status)
end
function metat.__index:receiveheaders()
return self.try(receiveheaders(self.c))
end
function metat.__index:receivebody(headers, sink, step)
sink = sink or ltn12.sink.null()
step = step or ltn12.pump.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
return self.try(ltn12.pump.all(socket.source(mode, self.c, length),
sink, step))
end
function metat.__index:receive09body(status, sink, step)
local source = ltn12.source.rewind(socket.source("until-closed", self.c))
source(status)
return self.try(ltn12.pump.all(source, sink, step))
end
function metat.__index:close()
return self.c:close()
end
-----------------------------------------------------------------------------
-- High level HTTP API
-----------------------------------------------------------------------------
local function adjusturi(reqt)
local u = reqt
-- if there is a proxy, we need the full url. otherwise, just a part.
if not reqt.proxy and not PROXY then
u = {
path = socket.try(reqt.path, "invalid path 'nil'"),
params = reqt.params,
query = reqt.query,
fragment = reqt.fragment
}
end
return url.build(u)
end
local function adjustproxy(reqt)
local proxy = reqt.proxy or PROXY
if proxy then
proxy = url.parse(proxy)
return proxy.host, proxy.port or 3128
else
return reqt.host, reqt.port
end
end
local function adjustheaders(reqt)
-- default headers
local lower = {
["user-agent"] = USERAGENT,
["host"] = reqt.host,
["connection"] = "close, TE",
["te"] = "trailers"
}
-- if we have authentication information, pass it along
if reqt.user and reqt.password then
lower["authorization"] =
"Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password))
end
-- override with user headers
for i,v in base.pairs(reqt.headers or lower) do
lower[string.lower(i)] = v
end
return lower
end
-- default url parts
local default = {
host = "",
port = PORT,
path ="/",
scheme = "http"
}
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
if nreqt.port == "" then nreqt.port = 80 end
socket.try(nreqt.host and nreqt.host ~= "",
"invalid host '" .. base.tostring(nreqt.host) .. "'")
-- compute uri if user hasn't overriden
nreqt.uri = reqt.uri or adjusturi(nreqt)
-- ajust host and port if there is a proxy
nreqt.host, nreqt.port = adjustproxy(nreqt)
-- adjust headers in request
nreqt.headers = adjustheaders(nreqt)
return nreqt
end
local function shouldredirect(reqt, code, headers)
return headers.location and
string.gsub(headers.location, "%s", "") ~= "" and
(reqt.redirect ~= false) and
(code == 301 or code == 302) and
(not reqt.method or reqt.method == "GET" or reqt.method == "HEAD")
and (not reqt.nredirects or reqt.nredirects < 5)
end
local function shouldreceivebody(reqt, code)
if reqt.method == "HEAD" then return nil end
if code == 204 or code == 304 then return nil end
if code >= 100 and code < 200 then return nil end
return 1
end
-- forward declarations
local trequest, tredirect
function tredirect(reqt, location)
local result, code, headers, status = trequest {
-- the RFC says the redirect URL has to be absolute, but some
-- servers do not respect that
url = url.absolute(reqt.url, location),
source = reqt.source,
sink = reqt.sink,
headers = reqt.headers,
proxy = reqt.proxy,
nredirects = (reqt.nredirects or 0) + 1,
create = reqt.create
}
-- pass location header back as a hint we redirected
headers = headers or {}
headers.location = headers.location or location
return result, code, headers, status
end
function trequest(reqt)
-- we loop until we get what we want, or
-- until we are sure there is no way to get it
local nreqt = adjustrequest(reqt)
local h = open(nreqt.host, nreqt.port, nreqt.create)
-- send request line and headers
h:sendrequestline(nreqt.method, nreqt.uri)
h:sendheaders(nreqt.headers)
-- if there is a body, send it
if nreqt.source then
h:sendbody(nreqt.headers, nreqt.source, nreqt.step)
end
local code, status = h:receivestatusline()
-- if it is an HTTP/0.9 server, simply get the body and we are done
if not code then
h:receive09body(status, nreqt.sink, nreqt.step)
return 1, 200
end
local headers
-- ignore any 100-continue messages
while code == 100 do
headers = h:receiveheaders()
code, status = h:receivestatusline()
end
headers = h:receiveheaders()
-- at this point we should have a honest reply from the server
-- we can't redirect if we already used the source, so we report the error
if shouldredirect(nreqt, code, headers) and not nreqt.source then
h:close()
return tredirect(reqt, headers.location)
end
-- here we are finally done
if shouldreceivebody(nreqt, code) then
h:receivebody(headers, nreqt.sink, nreqt.step)
end
h:close()
return 1, code, headers, status
end
local function srequest(u, b)
local t = {}
local reqt = {
url = u,
sink = ltn12.sink.table(t)
}
if b then
reqt.source = ltn12.source.string(b)
reqt.headers = {
["content-length"] = string.len(b),
["content-type"] = "application/x-www-form-urlencoded"
}
reqt.method = "POST"
end
local code, headers, status = socket.skip(1, trequest(reqt))
return table.concat(t), code, headers, status
end
request = socket.protect(function(reqt, body)
if base.type(reqt) == "string" then return srequest(reqt, body)
else return trequest(reqt) end
end)

View File

@@ -1,251 +0,0 @@
-----------------------------------------------------------------------------
-- SMTP client support for the Lua language.
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: smtp.lua,v 1.46 2007/03/12 04:08:40 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local coroutine = require("coroutine")
local string = require("string")
local math = require("math")
local os = require("os")
local socket = require("socket")
local tp = require("socket.tp")
local ltn12 = require("ltn12")
local mime = require("mime")
module("socket.smtp")
-----------------------------------------------------------------------------
-- Program constants
-----------------------------------------------------------------------------
-- timeout for connection
TIMEOUT = 60
-- default server used to send e-mails
SERVER = "localhost"
-- default port
PORT = 25
-- domain used in HELO command and default sendmail
-- If we are under a CGI, try to get from environment
DOMAIN = os.getenv("SERVER_NAME") or "localhost"
-- default time zone (means we don't know)
ZONE = "-0000"
---------------------------------------------------------------------------
-- Low level SMTP API
-----------------------------------------------------------------------------
local metat = { __index = {} }
function metat.__index:greet(domain)
self.try(self.tp:check("2.."))
self.try(self.tp:command("EHLO", domain or DOMAIN))
return socket.skip(1, self.try(self.tp:check("2..")))
end
function metat.__index:mail(from)
self.try(self.tp:command("MAIL", "FROM:" .. from))
return self.try(self.tp:check("2.."))
end
function metat.__index:rcpt(to)
self.try(self.tp:command("RCPT", "TO:" .. to))
return self.try(self.tp:check("2.."))
end
function metat.__index:data(src, step)
self.try(self.tp:command("DATA"))
self.try(self.tp:check("3.."))
self.try(self.tp:source(src, step))
self.try(self.tp:send("\r\n.\r\n"))
return self.try(self.tp:check("2.."))
end
function metat.__index:quit()
self.try(self.tp:command("QUIT"))
return self.try(self.tp:check("2.."))
end
function metat.__index:close()
return self.tp:close()
end
function metat.__index:login(user, password)
self.try(self.tp:command("AUTH", "LOGIN"))
self.try(self.tp:check("3.."))
self.try(self.tp:command(mime.b64(user)))
self.try(self.tp:check("3.."))
self.try(self.tp:command(mime.b64(password)))
return self.try(self.tp:check("2.."))
end
function metat.__index:plain(user, password)
local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password)
self.try(self.tp:command("AUTH", auth))
return self.try(self.tp:check("2.."))
end
function metat.__index:auth(user, password, ext)
if not user or not password then return 1 end
if string.find(ext, "AUTH[^\n]+LOGIN") then
return self:login(user, password)
elseif string.find(ext, "AUTH[^\n]+PLAIN") then
return self:plain(user, password)
else
self.try(nil, "authentication not supported")
end
end
-- send message or throw an exception
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
self:rcpt(v)
end
else
self:rcpt(mailt.rcpt)
end
self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step)
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)
-- make sure tp is closed if we get an exception
s.try = socket.newtry(function()
s:close()
end)
return s
end
-- convert headers to lowercase
local function lower_headers(headers)
local lower = {}
for i,v in base.pairs(headers or lower) do
lower[string.lower(i)] = v
end
return lower
end
---------------------------------------------------------------------------
-- Multipart message source
-----------------------------------------------------------------------------
-- returns a hopefully unique mime boundary
local seqno = 0
local function newboundary()
seqno = seqno + 1
return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'),
math.random(0, 99999), seqno)
end
-- send_message forward declaration
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
h = i .. ': ' .. v .. "\r\n" .. h
end
coroutine.yield(h)
end
-- yield multipart message body from a multipart message table
local function send_multipart(mesgt)
-- make sure we have our boundary and send headers
local bd = newboundary()
local headers = lower_headers(mesgt.headers or {})
headers['content-type'] = headers['content-type'] or 'multipart/mixed'
headers['content-type'] = headers['content-type'] ..
'; boundary="' .. bd .. '"'
send_headers(headers)
-- send preamble
if mesgt.body.preamble then
coroutine.yield(mesgt.body.preamble)
coroutine.yield("\r\n")
end
-- send each part separated by a boundary
for i, m in base.ipairs(mesgt.body) do
coroutine.yield("\r\n--" .. bd .. "\r\n")
send_message(m)
end
-- send last boundary
coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n")
-- send epilogue
if mesgt.body.epilogue then
coroutine.yield(mesgt.body.epilogue)
coroutine.yield("\r\n")
end
end
-- yield message body from a source
local function send_source(mesgt)
-- make sure we have a content-type
local headers = lower_headers(mesgt.headers or {})
headers['content-type'] = headers['content-type'] or
'text/plain; charset="iso-8859-1"'
send_headers(headers)
-- 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
end
end
-- yield message body from a string
local function send_string(mesgt)
-- make sure we have a content-type
local headers = lower_headers(mesgt.headers or {})
headers['content-type'] = headers['content-type'] or
'text/plain; charset="iso-8859-1"'
send_headers(headers)
-- send body from string
coroutine.yield(mesgt.body)
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
end
-- set defaul headers
local function adjust_headers(mesgt)
local lower = lower_headers(mesgt.headers)
lower["date"] = lower["date"] or
os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or ZONE)
lower["x-mailer"] = lower["x-mailer"] or socket._VERSION
-- this can't be overriden
lower["mime-version"] = "1.0"
return lower
end
function message(mesgt)
mesgt.headers = adjust_headers(mesgt)
-- create and return message source
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
end
end
---------------------------------------------------------------------------
-- High level SMTP API
-----------------------------------------------------------------------------
send = socket.protect(function(mailt)
local s = open(mailt.server, mailt.port, mailt.create)
local ext = s:greet(mailt.domain)
s:auth(mailt.user, mailt.password, ext)
s:send(mailt)
s:quit()
return s:close()
end)

View File

@@ -1,123 +0,0 @@
-----------------------------------------------------------------------------
-- Unified SMTP/FTP subsystem
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: tp.lua,v 1.22 2006/03/14 09:04:15 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local socket = require("socket")
local ltn12 = require("ltn12")
module("socket.tp")
-----------------------------------------------------------------------------
-- Program constants
-----------------------------------------------------------------------------
TIMEOUT = 60
-----------------------------------------------------------------------------
-- Implementation
-----------------------------------------------------------------------------
-- gets server reply (works for SMTP and FTP)
local function get_reply(c)
local code, current, sep
local line, err = c:receive()
local reply = line
if err then return nil, err end
code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)"))
if not code then return nil, "invalid server reply" end
if sep == "-" then -- reply is multiline
repeat
line, err = c:receive()
if err then return nil, err end
current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)"))
reply = reply .. "\n" .. line
-- reply ends with same code
until code == current and sep == " "
end
return code, reply
end
-- metatable for sock object
local metat = { __index = {} }
function metat.__index:check(ok)
local code, reply = get_reply(self.c)
if not code then return nil, reply end
if base.type(ok) ~= "function" then
if base.type(ok) == "table" then
for i, v in base.ipairs(ok) do
if string.find(code, v) then
return base.tonumber(code), reply
end
end
return nil, reply
else
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
end
function metat.__index:command(cmd, arg)
if arg then
return self.c:send(cmd .. " " .. arg.. "\r\n")
else
return self.c:send(cmd .. "\r\n")
end
end
function metat.__index:sink(snk, pat)
local chunk, err = c:receive(pat)
return snk(chunk, err)
end
function metat.__index:send(data)
return self.c:send(data)
end
function metat.__index:receive(pat)
return self.c:receive(pat)
end
function metat.__index:getfd()
return self.c:getfd()
end
function metat.__index:dirty()
return self.c:dirty()
end
function metat.__index:getcontrol()
return self.c
end
function metat.__index:source(source, step)
local sink = socket.sink("keep-open", self.c)
local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step)
return ret, err
end
-- closes the underlying c
function metat.__index:close()
self.c:close()
return 1
end
-- connect with server and return c object
function connect(host, port, timeout, create)
local c, e = (create or socket.tcp)()
if not c then return nil, e end
c:settimeout(timeout or TIMEOUT)
local r, e = c:connect(host, port)
if not r then
c:close()
return nil, e
end
return base.setmetatable({c = c}, metat)
end

View File

@@ -1,297 +0,0 @@
-----------------------------------------------------------------------------
-- URI parsing, composition and relative URL resolution
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: url.lua,v 1.38 2006/04/03 04:45:42 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module
-----------------------------------------------------------------------------
local string = require("string")
local base = _G
local table = require("table")
module("socket.url")
-----------------------------------------------------------------------------
-- Module version
-----------------------------------------------------------------------------
_VERSION = "URL 1.0.1"
-----------------------------------------------------------------------------
-- Encodes a string into its escaped hexadecimal representation
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
function escape(s)
return string.gsub(s, "([^A-Za-z0-9_])", function(c)
return string.format("%%%02x", string.byte(c))
end)
end
-----------------------------------------------------------------------------
-- Protects a path segment, to prevent it from interfering with the
-- url parsing.
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
local function make_set(t)
local s = {}
for i,v in base.ipairs(t) do
s[t[i]] = 1
end
return s
end
-- these are allowed withing a path segment, along with alphanum
-- other characters must be escaped
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
end)
end
-----------------------------------------------------------------------------
-- Encodes a string into its escaped hexadecimal representation
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
function unescape(s)
return string.gsub(s, "%%(%x%x)", function(hex)
return string.char(base.tonumber(hex, 16))
end)
end
-----------------------------------------------------------------------------
-- Builds a path from a base path and a relative path
-- Input
-- base_path
-- relative_path
-- Returns
-- corresponding absolute path
-----------------------------------------------------------------------------
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)
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)
if s ~= "../../" then return "" else return s end
end)
end
path = string.gsub(reduced, "([^/]*/%.%.)$", function (s)
if s ~= "../.." then return "" else return s end
end)
return path
end
-----------------------------------------------------------------------------
-- Parses a url and returns a table with all its parts according to RFC 2396
-- The following grammar describes the names given to the URL parts
-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
-- <authority> ::= <userinfo>@<host>:<port>
-- <userinfo> ::= <user>[:<password>]
-- <path> :: = {<segment>/}<segment>
-- Input
-- url: uniform resource locator of request
-- default: table with default values for each field
-- Returns
-- table with the following fields, where RFC naming conventions have
-- been preserved:
-- scheme, authority, userinfo, user, password, host, port,
-- path, params, query, fragment
-- Obs:
-- the leading '/' in {/<path>} is considered part of <path>
-----------------------------------------------------------------------------
function parse(url, default)
-- initialize default parameters
local parsed = {}
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
-- url = string.gsub(url, "%s", "")
-- get fragment
url = string.gsub(url, "#(.*)$", function(f)
parsed.fragment = f
return ""
end)
-- get scheme
url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
function(s) parsed.scheme = s; return "" end)
-- get authority
url = string.gsub(url, "^//([^/]*)", function(n)
parsed.authority = n
return ""
end)
-- get query stringing
url = string.gsub(url, "%?(.*)", function(q)
parsed.query = q
return ""
end)
-- get params
url = string.gsub(url, "%;(.*)", function(p)
parsed.params = p
return ""
end)
-- path is whatever was left
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(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)
parsed.user = userinfo
return parsed
end
-----------------------------------------------------------------------------
-- Rebuilds a parsed URL from its components.
-- Components are protected if any reserved or unallowed characters are found
-- Input
-- parsed: parsed URL, as returned by parse
-- Returns
-- a stringing with the corresponding URL
-----------------------------------------------------------------------------
function build(parsed)
local ppath = parse_path(parsed.path or "")
local url = build_path(ppath)
if parsed.params then url = url .. ";" .. parsed.params end
if parsed.query then url = url .. "?" .. parsed.query end
local authority = parsed.authority
if parsed.host then
authority = parsed.host
if parsed.port then authority = authority .. ":" .. parsed.port end
local userinfo = parsed.userinfo
if parsed.user then
userinfo = parsed.user
if parsed.password then
userinfo = userinfo .. ":" .. parsed.password
end
end
if userinfo then authority = userinfo .. "@" .. authority end
end
if authority then url = "//" .. authority .. url end
if parsed.scheme then url = parsed.scheme .. ":" .. url end
if parsed.fragment then url = url .. "#" .. parsed.fragment end
-- url = string.gsub(url, "%s", "")
return url
end
-----------------------------------------------------------------------------
-- Builds a absolute URL from a base and a relative URL according to RFC 2396
-- Input
-- base_url
-- relative_url
-- Returns
-- corresponding absolute url
-----------------------------------------------------------------------------
function absolute(base_url, relative_url)
if base.type(base_url) == "table" then
base_parsed = base_url
base_url = build(base_parsed)
else
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
else
relative_parsed.scheme = base_parsed.scheme
if not relative_parsed.authority then
relative_parsed.authority = base_parsed.authority
if not relative_parsed.path then
relative_parsed.path = base_parsed.path
if not relative_parsed.params then
relative_parsed.params = base_parsed.params
if not relative_parsed.query then
relative_parsed.query = base_parsed.query
end
end
else
relative_parsed.path = absolute_path(base_parsed.path or "",
relative_parsed.path)
end
end
return build(relative_parsed)
end
end
-----------------------------------------------------------------------------
-- Breaks a path into its segments, unescaping the segments
-- Input
-- path
-- Returns
-- segment: a table with one entry per segment
-----------------------------------------------------------------------------
function parse_path(path)
local parsed = {}
path = path or ""
--path = string.gsub(path, "%s", "")
string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end)
for i = 1, table.getn(parsed) do
parsed[i] = unescape(parsed[i])
end
if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end
if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end
return parsed
end
-----------------------------------------------------------------------------
-- Builds a path component from its segments, escaping protected characters.
-- Input
-- parsed: path segments
-- unsafe: if true, segments are not protected before path is built
-- Returns
-- path: corresponding path stringing
-----------------------------------------------------------------------------
function build_path(parsed, unsafe)
local path = ""
local n = table.getn(parsed)
if unsafe then
for i = 1, n-1 do
path = path .. parsed[i]
path = path .. "/"
end
if n > 0 then
path = path .. parsed[n]
if parsed.is_directory then path = path .. "/" end
end
else
for i = 1, n-1 do
path = path .. protect_segment(parsed[i])
path = path .. "/"
end
if n > 0 then
path = path .. protect_segment(parsed[n])
if parsed.is_directory then path = path .. "/" end
end
end
if parsed.is_absolute then path = "/" .. path end
return path
end

211
readme.md
View File

@@ -7,18 +7,17 @@ Lua scripting for Blockland
- Install RedBlocklandLoader
- Copy `lua5.1.dll` into your Blockland install folder, next to `Blockland.exe`
- Copy `BlockLua.dll` into the `modules` folder within the Blockland folder
- Optional: Copy the `lualib` folder into `modules`
## Quick Reference
### From TorqueScript
`'print('hello world')` - Execute Lua code in the console by prepending a `'` (single quote)
`luaeval("code");` - Eval Lua code
`luacall("funcName", %args);` - Call a Lua global function
`luaexec("fileName");` - Execute a Lua file. Path rules are the same as executing .cs files.
`luaget("varName");` - Read a Lua global variable
`luaset("varName");` - Write a Lua global variable
`'print('hello world')` - Execute Lua in the console by prepending a `'` (single quote)
`luaeval("code");` - Execute Lua code
`luacall("funcName", %args...);` - Call a Lua function (supports indexing tables and object methods)
`luaexec("fileName");` - Execute a Lua file. Path rules are the same as when executing .cs files, relative paths are allowed.
`luaget("varName");` - Read a Lua global variable (supports indexing tables)
`luaset("varName", %value);` - Write a Lua global variable (supports indexing tables)
### From Lua
`bl.eval('code')` - Eval TorqueScript code
@@ -26,6 +25,7 @@ Lua scripting for Blockland
`bl.varName` - Read a TorqueScript global variable
`bl['varName']` - Read a TorqueScript global variable (i.e. with special characters in the name, or from an array)
`bl.set('varName', value)` - Write a TorqueScript global variable
`bl['namespaceName::funcName'](args)` - Call a namespaced TorqueScript function
### Accessing Torque Objects from Lua
`bl.objectName` - Access a Torque object by name
@@ -35,29 +35,49 @@ Lua scripting for Blockland
`object.key = value` - Associate Lua data with a Torque object
`object:method(args)` - Call a Torque object method
`object[index]` - Access a member of a Torque set or group
`for childIndex, child in object:members() do` - Iterate objects within of a Torque set or group. Indices start at 0 like in Torque.
`for child in object:members() do` - Iterate objects within of a Torque set or group. Indices start at 0 like in Torque.
`bl.isObject(object, objectID, or 'objectName')` - Check if an object exists
`object:exists()` - Check if an object exists
### Advanced
### Timing/Schedules
`sched = bl.schedule(timeMs, function, args...)` - Schedule a Lua function to be called later, similar to schedule in Torque
`sched:cancel()` - Cancel a previously scheduled timer
`hitObject, hisPos, hitNormal = bl.raycast(vector{startPosX,y,z}, vector{endPosX,y,z}, 'objtype'/{'objtypes',...}, ignoreObjects...?)` - Cast a ray in the world over objects of the specified type(s) (possibly excluding some objects), and return the object hit, the position of the hit, and the normal vector to the surface hit. See the Types section for a list of valid object types.
### Raycasts and Searches
`hitObject, hitPos, hitNormal = bl.raycast(vector{startPosX,y,z}, vector{endPosX,y,z}, 'objtype'/{'objtypes',...}, ignoreObjects...?)` - Cast a ray in the world over objects of the specified type(s) (possibly excluding some objects), and return the object hit, the position of the hit, and the normal vector to the surface hit. See the Types section for a list of valid object types.
`for object in bl.boxSearch(vector{centerX,y,z}, vector{sizeX,y,z}, 'objtype'/{'objtypes',...}) do` - Find all objects in the world of the specified type(s) whose bounding box overlaps with the specified box. See the Types section for a list of valid object types.
`for object in bl.radiusSearch(vector{centerX,y,z}, radius, 'objtype'/{'objtypes',...}) do` - Find all objects of the specified type(s) whose bounding box overlaps with the specified sphere. See the Types section for a list of valid object types.
`bl.serverCmd('commandName', function(client, args...) code() end)` - Register a /-command on the server
`bl.clientCmd('commandName', function(args...) code() end)` - Register a client command on the client
### Server-Client Communication
`bl.addServerCmd('commandName', function(client, args...) ... end)` - Register a /command on the server
`bl.addClientCmd('commandName', function(args...) ... end)` - Register a client command on the client
`bl.commandToServer('commandName', args...)` - Execute a server command as a client
`bl.commandToClient('commandName', args...)` - As the server, execute a client command on a specific client
`bl.commandToAll('commandName', args...)` - As the server, execute a client command on all clients
### Packages/Hooks
`bl.hook('packageName', 'functionName', 'before'/'after'/'override', function(args...) code() end)` - Hook a Torque function with a Lua function
`bl.unhook('packageName', 'functionName', 'before'/'after'/'override')` - Remove a previously defined hook
`bl.hook('packageName', 'functionName', 'before'/'after', function(args) ... end)` - Hook a Torque function with a Lua function.
`args` is an array containing the arguments provided to the function. If the hook is `before`, these can be modified before being passed to the parent function.
If `args._return` is set to anything other than nil by a `before` hook, the parent function will not be called, and the function will simply return that value. Also in this case, any `after` hook will not be executed.
In an `after` hook, `args._return` is set to the value returned by the parent function, and can be modified.
`bl.unhook('packageName', 'functionName', 'before'/'after')` - Remove a previously defined hook
`bl.unhook('packageName', 'functionName')` - Remove any previously defined hooks on the function within the package
`bl.unhook('packageName')` - Remove any previously defined hooks within the package
### Classes and Types
`bl.bool(thing)` - Convert a Torque boolean (0 or 1) into a Lua boolean. Done automatically for all built-in functions that return bools.
`bl.object(thing)` - Convert a Torque object reference (object ID or name) into a Lua object. Done automatically for all built-in functions that return objects.
`bl.type('varName', 'type')` - Register the type of a Torque global variable, for conversion when accessing from Lua. Valid types are 'bool', 'object', and nil - all other conversion is automatic.
`bl.type('funcName', 'type')` - Register the return type of a Torque function, for conversion when calling from Lua. Valid types are 'bool', 'object', and nil - all other conversion is automatic. Already done for all built-in functions that return objects.
`bl.type('className::funcName', 'type')` - Register the return type of a Torque object method.
`bl.class('className')` - Register a Torque class to be used from Lua (Already done for all built-in classes)
`bl.class('className', 'parentClassName')` - Same as above, with inheritance
### Modules and Dependencies
`dofile('Add-Ons/Path/file.lua')` - Execute a Lua file. Relative paths (`./file.lua`) are allowed. `..` is not allowed.
`require('modulePath.moduleName')` - Load a Lua file or external library.
`require` replaces `.` with `/` in the path, and then searches for files in the following order:
- `./modulePath/moduleName.lua`
- `./modulePath/moduleName/init.lua`
- `modulePath/moduleName.lua` (Relative to game directory)
- `modulePath/moduleName/init.lua` (Relative to game directory)
- `modules/lualib/modulePath/moduleName.lua`
- `modules/lualib/modulePath/moduleName/init.lua`
- `modules/lualib/modulePath/moduleName.dll` - C libraries for Lua can be loaded
Like in standard Lua, modules loaded using `require` are only executed the first time `require` is called with that path (from anywhere). Subsequent calls simply return the result from the initial execution. To allow hot reloading, use `dofile`.
### File I/O
Lua's builtin file I/O is emulated, and is confined to the same directories as TorqueScript file I/O.
@@ -69,54 +89,131 @@ Relative paths (`./`) are allowed. `..` is not allowed.
Reading files from ZIPs is supported, with caveats. Null characters are not allowed, and \r\n becomes \n. Generally, text formats work, and binary formats don't.
When reading from outside ZIPs, binary files are fully supported.
### Modules and Dependencies
`dofile('Add-Ons/Path/file.lua')` - Execute a Lua file. Relative paths (`./file.lua`) are allowed. `..` is not allowed.
`require('modulePath.moduleName')` - Load a Lua file or or external library.
Require replaces `.` with `/` in the path, and then searches for files in the following order:
- `./modulePath/moduleName.lua`
- `./modulePath/moduleName/init.lua`
- `modulePath/moduleName.lua` (Relative to game directory)
- `modulePath/moduleName/init.lua` (Relative to game directory)
- `modules/lualib/modulePath/moduleName.lua`
- `modules/lualib/modulePath/moduleName/init.lua`
- `modules/lualib/modulePath/moduleName.dll`
Like in standard Lua, modules loaded using `require` are only executed the first time `require` is called with that path. Subsequent calls simply return the result from the initial execution. To allow hot reloading, use `dofile`.
### Object Creation
`bl.new('className')` - Create a new Torque object
`bl.new('className', {fieldName = value, ...})` - Create a new Torque object with the given fields
`bl.new('className objectName', fields?)` - Create a new named Torque object
`bl.new('className objectName:parentName', fields?)` - Create a new named Torque object with inheritance
`bl.datablock('datablockClassName datablockName', fields?)` - Create a new datablock
`bl.datablock('datablockClassName datablockName:parentDatablockName', fields?)` - Create a new datablock with inheritance
### Classes and Types
`bl.type('varName', 'type')` - Register the type of a Torque global variable, for conversion when accessing from Lua. Valid types are 'boolean', 'object', 'string' (prevents automatic conversion), and nil (default, applies automatic conversion).
`bl.type('funcName', 'type')` - Register the return type of a Torque function, for conversion when calling from Lua. Valid types are 'bool', 'object', and nil - all other conversion is automatic. Already done for all default functions.
`bl.type('className::funcName', 'type')` - Register the return type of a Torque object method.
`bl.class('className')` - Register an existing Torque class to be used from Lua. Already done for all built-in classes.
`bl.class('className', 'parentClassName')` - Same as above, with inheritance
`bl.boolean(arg)` - Manually convert a Torque boolean (0 or 1) into a Lua boolean.
`bl.object(arg)` - Manually convert a Torque object reference (object ID or name) into a Lua object.
`bl.string(arg)` - Manually convert any automatically-converted Torque value back into a string. This is not as reliable as using `bl.type` to specify the type as a string beforehand.
### Vector
`vec = vector{x,y,z}` - Create a vector. Can have any number of elements
`vec1 + vec2` - Add
`vec1 - vec2` - Subtract
`vec * number` - Scale (x\*n, y\*n, z\*n)
`vec / number` - Scale (x/n, y/n, z/n)
`vec ^ number` - Exponentiate (x^n, y^n, z^n)
`vec1 * vec2` - Multiply elements piecewise (x1\*x2, y1\*y2, z1\*z2)
`vec1 / vec2` - Divide elements piecewise (x1/x2, y1/y2, z1/z2)
`vec1 ^ vec2` - Exponentiate elements piecewise (x1^x2, y1^y2, z1^z2)
`-vec` - Negate/invert
`vec1 == vec2` - Compare by value
`vec:length()` - Length
`vec:normalize()` - Preserve direction but scale so magnitude is 1
`vec:floor()` - Floor each element
`vec:ceil()` - Ceil each element
`vec:abs()` - Absolute value each element
`vec1:dot(vec2)` - Dot product
`vec1:cross(vec2)` - Cross product (Only defined for two vectors of length 3)
`vec:rotateZ(radians)` - Rotate counterclockwise about the Z (vertical) axis
`vec:rotateByAngleId(0/1/2/3)` - Rotate counterclockwise about the Z (vertical) axis in increments of 90 degrees
`vec:tsString()` - Convert to string usable by Torque. Done automatically when a vector is passed to Torque.
`vec1:distance(vec2)` - Distance between two points
`vec2 = vec:copy()` - Clone a vector so its elements can be modified without affecting the original. Usually not needed - the builtin vector functions never modify vectors in-place.
### Matrix
WIP
### Extended Standard Lua Library
`string[index]`
`string[{start,stop}]`
`string.split(str, separator='' (splits into chars), noregex=false)`
`string.bytes(str)`
`string.trim(str, charsToTrim=' \t\r\n')`
`table.empty`
`table.map(func, ...)`
`table.mapk(func, ...)`
`table.map_list(func, ...)`
`table.mapi_list(func, ...)`
`table.swap(tbl)`
`table.reverse(list)`
`table.islist(list)`
`table.append(list, ...)`
`table.join(...)`
`table.contains(tbl, val)`
`table.contains_list(list, val)`
`table.copy(tbl)`
`table.copy_list(list)`
`table.sortcopy(tbl, sortFunction?)`
`table.removevalue(tbl, val)`
`table.removevalue_list(tbl, val)`
`table.tostring(tbl)`
`for char in string.chars(str) do`
`string.escape(str, escapes={['\n']='\\n', etc. (C standard)})`
`string.unescape(str, escapeChar='\\', unescapes={['\\n']='\n', etc.})`
`io.readall(filename)`
`io.writeall(filename, str)`
`math.round(num)`
`math.mod(divisor, modulus)`
`math.clamp(num, min, max)`
## Type Conversion
When a TorqueScript function is called from Lua or vice-versa, the arguments and return value must be converted between the two languages' type systems.
TorqueScript stores no type information; all values in TorqueScript are strings. So it's necessary to make some inferences when converting values between the two languages.
### From TorqueScript to Lua
- Any numeric value becomes a Lua `number`, except as specified with `ts.type`, which may convert a value into a `boolean` or a Torque object container.
- The empty string "" becomes `nil`
- A string containing three numbers separated by spaces becomes a `vector`
- A string containing six numbers separated by spaces becomes a table of two vectors
- Any other string is passed directly as a `string`
### From Lua to TorqueScript
- `nil` becomes the empty string ""
- `true` and `false` become "1" and "0" respectively
- Torque containers become their object ID
- A Torque object container becomes its object ID
- A `vector` becomes a string containing three numbers separated by spaces
- A table of two vectors becomes a string containing six numbers separated by spaces
- A table of two `vector`s becomes a string containing six numbers separated by spaces
- (WIP) A `matrix` is converted into an axis-angle (a "transform"), a string containing seven numbers separated by spaces
- Any `string` is passed directly as a string
- Tables cannot be passed and will throw an error
### From TorqueScript to Lua
- The empty string "" becomes `nil`
- Any numeric value becomes a Lua `number`, except as specified with `bl.type`, which may convert a value into a `boolean` or a Torque object container.
- A string containing two or three numbers separated by single spaces becomes a `vector`
- A string containing six numbers separated by single spaces becomes a table of two vectors, usually defining the corners a bounding box
- (WIP) A string containing seven numbers separated by single spaces is treated as an axis-angle (a "transform"), and is converted into a `matrix` representing the translation and rotation
- Any other string is passed directly as a `string`
For scenarios where the automatic TorqueScript->Lua conversion rules are insufficient or incorrect, use `bl.type`.
To convert things by hand, use `bl.object`, `bl.boolean`, or `bl.string`.
## I/O and Safety
All Lua code is sandboxed, and file access is confied to the default directories in the same way TorqueScript is.
All Lua code is sandboxed, and file access is confined to the default directories in the same way TorqueScript is.
BlockLua also has access to any C libraries installed in the `modules/lualib` folder, so be careful throwing things in there.
### Unsafe Mode
BlockLua-Unsafe.dll can be used in place of BlockLua.dll, to remove the sandboxing of Lua code. This allows Lua code to access any file and use any library, including ffi.
Please do not publish add-ons that require unsafe mode.
## Other Reference
BlockLua can be built in Unsafe Mode by specifying the `-DBLLUA_UNSAFE` compiler flag. This removes the sandboxing of Lua code, allowing it to access any file and use any library, including ffi.
A more limited option is `-DBLLUA_ALLOWFFI`, which allows the use of the `ffi` library. This can still be exploited to grant all the same access as full unsafe mode.
Please do not publish add-ons that require either of these.
### List of Object Types
'all' - Any object
'brick' - Bricks with Ray-Casting enabled
'brickalways' - All bricks including those with Ray-Casting disabled
'player' - Players or bots
'item' - Items
'vehicle' - Vehicles
'projectile' - Projectiles
Other types: 'static', 'environment', 'terrain', 'water', 'trigger', 'marker', 'gamebase', 'shapebase', 'camera', 'staticshape', 'vehicleblocker', 'explosion', 'corpse', 'debris', 'physicalzone', 'staticts', 'staticrendered', 'damagableitem'
`'all'` - Any object
`'player'` - Players or bots
`'item'` - Items
`'vehicle'` - Vehicles
`'projectile'` - Projectiles
`'brick'` - Bricks with raycasting enabled
`'brickalways'` - All bricks including those with raycasting disabled
Other types: `'static'`, `'environment'`, `'terrain'`, `'water'`, `'trigger'`, `'marker'`, `'gamebase'`, `'shapebase'`, `'camera'`, `'staticshape'`, `'vehicleblocker'`, `'explosion'`, `'corpse'`, `'debris'`, `'physicalzone'`, `'staticts'`, `'staticrendered'`, `'damagableitem'`
## Compiling
With any *32-bit* variant of GCC installed (such as MinGW or MSYS2), run the following command in the repo directory:
`g++ src/bllua4.cpp -o BlockLua.dll -m32 -shared -static-libgcc -Isrc -Iinc/tsfuncs -Iinc/lua -lpsapi -L. -llua5.1`
LuaJIT (lua5.1.dll) can be obtained from https://luajit.org/

View File

@@ -1,4 +1,4 @@
// BlockLua (bllua4): Simple Lua interface for TorqueScript
// BlockLua (bllua4): Advanced Lua interface for TorqueScript
// Includes
@@ -28,8 +28,9 @@ INCLUDE_BIN(bll_fileLuaEnv , "lua-env.lua");
INCLUDE_BIN(bll_fileTsEnv , "ts-env.cs" );
INCLUDE_BIN(bll_fileLuaStd , "util/std.lua");
INCLUDE_BIN(bll_fileLuaVector , "util/vector.lua");
INCLUDE_BIN(bll_fileLuaLibts , "util/libts.lua");
INCLUDE_BIN(bll_fileTsLibtsSupport, "util/libts-support.cs");
INCLUDE_BIN(bll_fileLuaMatrix , "util/matrix.lua");
INCLUDE_BIN(bll_fileLuaLibts , "util/libts-lua.lua");
INCLUDE_BIN(bll_fileTsLibts , "util/libts-ts.cs");
INCLUDE_BIN(bll_fileLuaLibbl , "util/libbl.lua" );
INCLUDE_BIN(bll_fileLuaLibblTypes , "util/libbl-types.lua");
INCLUDE_BIN(bll_fileTsLibblSupport, "util/libbl-support.cs");
@@ -56,21 +57,26 @@ bool init() {
// Set up Lua environment
BLL_LOAD_LUA(gL, bll_fileLuaEnv);
#ifdef BLLUA_ALLOWFFI
lua_pushboolean(gL, true);
lua_setglobal(gL, "_bllua_allowffi");
#endif
#ifndef BLLUA_UNSAFE
BLL_LOAD_LUA(gL, bll_fileLuaEnvSafe);
#endif
// Load utilities in Lua
BLL_LOAD_LUA(gL, bll_fileLuaStd);
BLL_LOAD_LUA(gL, bll_fileLuaVector);
BLL_LOAD_LUA(gL, bll_fileLuaMatrix);
BLL_LOAD_LUA(gL, bll_fileLuaLibts);
BLL_LOAD_LUA(gL, bll_fileLuaLibbl);
BLL_LOAD_LUA(gL, bll_fileLuaLibblTypes);
// Expose Lua API to TS
BlAddFunction(NULL, NULL, "_bllua_luacall", bll_ts_luacall, "LuaCall(name, ...) - Call Lua function and return result", 2, 20);
BlEval(bll_fileTsEnv);
// Load utilities
BLL_LOAD_LUA(gL, bll_fileLuaStd);
BLL_LOAD_LUA(gL, bll_fileLuaVector);
BLL_LOAD_LUA(gL, bll_fileLuaLibts);
BlEval(bll_fileTsLibtsSupport);
BLL_LOAD_LUA(gL, bll_fileLuaLibbl);
BLL_LOAD_LUA(gL, bll_fileLuaLibblTypes);
BlEval(bll_fileTsLibts);
BlEval(bll_fileTsLibblSupport);
BlEval(bll_fileLoadaddons);
@@ -83,8 +89,7 @@ bool init() {
bool deinit() {
BlPrintf("BlockLua: Unloading");
BlEval("deactivatePackage(_bllua_main);");
BlEval("$_bllua_active = 0;");
BlEval("$_bllua_active=0;deactivatePackage(_bllua_main);");
bll_LuaEval(gL, "for _,f in pairs(_bllua_on_unload) do f() end");
lua_close(gL);

View File

@@ -12,6 +12,7 @@ local old_require = require
local old_os = os
local old_debug = debug
local old_package = package
local old_allowffi = _bllua_allowffi
-- Remove all global variables except a whitelist
local ok_names = tmap {
@@ -37,13 +38,10 @@ end
-- Sanitize file paths to point only to allowed files within the game directory
-- List of allowed directories for reading/writing
-- modules/lualib is also allowed as read-only
local allowed_dirs = tmap {
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
}
-- List of allowed directories for reading only
local allowed_dirs_readonly = tmap {
'lualib'
}
-- List of disallowed file extensions - basically executable file extensions
-- Note that even without this protection, exploiting would still require somehow
-- getting a file within the allowed directories to autorun,
@@ -79,14 +77,15 @@ local function safe_path(fn, readonly)
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')
if not (dir and (
allowed_dirs[dir:lower()] or
( readonly and fn:find('^modules/lualib/') ) ))
then
return nil, 'File is in disallowed directory '..(dir or 'nil')
end
-- disallow blacklisted extensions or no extension
-- disallow blacklisted extensions
local ext = fn:match('%.([^/%.]+)$')
if (not ext) or (disallowed_exts[ext:lower()]) then
if ext and disallowed_exts[ext:lower()] then
return nil, 'Filename \''..fn..'\' has disallowed extension \''..
(ext or '')..'\''
end
@@ -117,6 +116,7 @@ local disallowed_packages = tmap {
'ffi', 'debug', 'package', 'io', 'os',
'_bllua_ts',
}
if old_allowffi then disallowed_packages['ffi'] = nil end
function _bllua_requiresecure(name)
if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or
name:find('^%.') or name:find('%.$') then
@@ -139,4 +139,5 @@ debug = {
getfilename = old_debug.getfilename, -- defined in lua.env.lua
}
_bllua_ts.echo(' Executed bllua-env-safe.lua')
print = _bllua_ts.echo
print(' Executed bllua-env-safe.lua')

View File

@@ -37,4 +37,9 @@ function _bllua_on_error(err)
return table.concat(tracelines, '\n')
end
_bllua_ts.echo(' Executed bllua-env.lua')
-- overridden in lua-env-safe.lua (executed if not BLLUA_UNSAFE)
_bllua_io_open = io.open
_bllua_requiresecure = require
print = _bllua_ts.echo
print(' Executed bllua-env.lua')

View File

@@ -1,26 +0,0 @@
// Built-in functions
// Eval'd after BLLua4 has loaded the Lua environment and API
// Public Lua library for TS
function luacall(%func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p) {
if($_bllua_active)
return _bllua_luacall(%func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p);
}
function luaexec(%fn) {
if($_bllua_active)
return _bllua_luacall("_bllua_exec", %fn);
}
function luaeval(%code) {
if($_bllua_active)
return _bllua_luacall("_bllua_eval", %code);
}
function luaget(%name) {
if($_bllua_active)
return _bllua_luacall("_bllua_getvar", %name);
}
function luaset(%name, %val) {
if($_bllua_active)
_bllua_luacall("_bllua_setvar", %name, %val);
}
echo(" Executed bllua-env.cs");

View File

@@ -1,7 +1,7 @@
// Private - Utilities used by libbl from the Lua side
package _bllua_smartEval {
package smartEval {
// Allow prepending ' to console commands to eval in lua instead of TS
// Also wraps TS lines with echo(...); if they don't end in ; or }
function ConsoleEntry::eval() {
@@ -10,7 +10,7 @@ package _bllua_smartEval {
if($_bllua_active) {
%text = getSubStr(%text, 1, strLen(%text));
echo("Lua ==> " @ %text);
luacall("_bllua_smarteval", %text);
_bllua_luacall("_bllua_smarteval", %text);
} else {
echo("Lua: not loaded");
}
@@ -28,7 +28,7 @@ package _bllua_smartEval {
}
}
};
activatePackage(_bllua_smartEval);
activatePackage(smartEval);
package _bllua_objectDeletionHook {
// Hook object deletion to clean up its lua data
@@ -36,9 +36,32 @@ package _bllua_objectDeletionHook {
// note: no parent function exists by default,
// and this is loaded before any addons
//parent::onRemove(%obj);
if($_bllua_active) luacall("_bllua_objectDeleted", %obj);
// assuming obj is an ID and never a name
if($_bllua_active) _bllua_luacall("_bllua_objectDeleted", %obj);
}
};
activatePackage(_bllua_objectDeletionHook);
// Public Lua library for TS
function luacall(%func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p) {
if($_bllua_active)
return _bllua_luacall("_bllua_call", %func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p);
}
function luaexec(%fn) {
if($_bllua_active)
return _bllua_luacall("_bllua_exec", %fn);
}
function luaeval(%code) {
if($_bllua_active)
return _bllua_luacall("_bllua_eval", %code);
}
function luaget(%name) {
if($_bllua_active)
return _bllua_luacall("_bllua_getvar", %name);
}
function luaset(%name, %val) {
if($_bllua_active)
_bllua_luacall("_bllua_setvar", %name, %val);
}
echo(" Executed libbl-support.cs");

File diff suppressed because it is too large Load Diff

View File

@@ -2,23 +2,36 @@
-- Main lua-side functionality of bllua,
-- provided through the global table 'bl.'
-- todo: set
local _bllua_ts = ts
bl = bl or {}
-- Config
local tsMaxArgs = 16
local tsArgsLocal = '%a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p'
local tsArgsGlobal =
'$_bllua_hook_arg1,$_bllua_hook_arg2,$_bllua_hook_arg3,$_bllua_hook_arg4,'..
'$_bllua_hook_arg5,$_bllua_hook_arg6,$_bllua_hook_arg7,$_bllua_hook_arg8,'..
'$_bllua_hook_arg9,$_bllua_hook_arg10,$_bllua_hook_arg11,$_bllua_hook_arg12,'..
'$_bllua_hook_arg13,$_bllua_hook_arg14,$_bllua_hook_arg15,$_bllua_hook_arg16'
-- Misc
-- Apply a function to each element in a list, building a new list from the returns
local function map(t,f)
local u = {}
for i,v in ipairs(t) do
u[i] = f(v)
local function ipairsNilable(t)
local maxk = 0
for k,_ in pairs(t) do
if k>maxk then maxk = k end
end
local i = 0
return function()
i = i+1
if i>maxk then return nil
else return i, t[i] end
end
return u
end
-- Validation
local function isValidFuncName(name)
return type(name)=='string' and name:find('^[a-zA-Z0-9_]+$')
return type(name)=='string' and name:find('^[a-zA-Z_][a-zA-Z0-9_]*$')
end
local function isValidFuncNameNs(name)
return type(name)=='string' and (
@@ -76,7 +89,7 @@ for k,v in pairs(tsTypesByName) do
tsTypesByNum[v] = k
end
-- Type conversion
-- Type conversion from Lua to TS
local toTsObject
-- Convert a string from TS into a boolean
-- Note: Nonempty nonnumeric strings evaluate to 1, unlike in TS
@@ -100,27 +113,77 @@ local function valToTs(val)
-- box - > 6 numbers
return table.concat(val[1], ' ')..' '..table.concat(val[2], ' ')
else
error('valToTs: could not convert table', 3)
error('valToTs: cannot pass Lua tables to TorqueScript.', 3)
end
else
error('valToTs: could not convert '..type(val), 3)
error('valToTs: could not convert value to TorqueScript: '..tostring(val), 3)
end
end
-- Type conversion from TS to Lua
local fromTsForceTypes = {
['boolean'] = function(val) return tsBool(val) end,
['object'] = function(val) return toTsObject(val) end, -- wrap because toTsObject not defined yet
['string'] = function(val) return val end,
}
local function forceValFromTs(val, typ)
local func = fromTsForceTypes[typ]
if not func then error('valFromTs: invalid force type \''..typ..'\'', 4) end
return func(val)
end
local function vectorFromTs(val)
local xS,yS,zS = val:match('^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$')
if xS then return vector{tonumber(xS),tonumber(yS),tonumber(zS)}
else return nil end
end
local function boxFromTs(val)
local x1S,y1S,z1S,x2S,y2S,z2S = val:match(
'^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+) '..
'(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$')
if x1S then return {
vector{tonumber(x1S),tonumber(y1S),tonumber(z1S)},
vector{tonumber(x2S),tonumber(y2S),tonumber(z2S)} }
else return nil end
end
local function multinumericFromTs(val)
local tsNumPat = '%-?[0-9]+%.?[0-9]*e?[0-9]*'
if val:find('^'..tsNumPat) then
local nums = {}
for _,part in ipairs(val:split(' ')) do
if part:find('^'..tsNumPat..'$') then
table.insert(nums, tonumber(part))
else
return nil
end
end
if #nums==2 or #nums==3 then
return vector(nums)
elseif #nums==6 then -- box
return {
vector{nums[1], nums[2], nums[3]},
vector{nums[4], nums[5], nums[6]} }
elseif #nums==7 then -- axis
return nil --return matrix(nums)
else
return nil
end
end
end
bl._forceType = bl._forceType or {}
local function valFromTs(val, name)
-- todo: ensure name and name2 are already lowercase
local function valFromTs(val, name, name2)
if type(val)~='string' then
error('valFromTs: expected string, got '..type(val), 3) end
if name then
local nameL = name:lower()
if bl._forceType[nameL] then
local typ = bl._forceType[nameL]
if typ=='boolean' then
return tsBool(val)
elseif typ=='object' then
return toTsObject(val)
else
error('valFromTs: invalid force type '..typ, 3)
end
name = name:lower()
if bl._forceType[name] then
return forceValFromTs(val, bl._forceType[name])
end
end
if name2 then
name2 = name2:lower()
if bl._forceType[name2] then
return forceValFromTs(val, bl._forceType[name2])
end
end
-- '' -> nil
@@ -128,45 +191,83 @@ local function valFromTs(val, name)
-- number
local num = tonumber(val)
if num then return num end
-- vector
local xS,yS,zS = val:match('^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$')
if xS then return vector{tonumber(xS),tonumber(yS),tonumber(zS)} end
local x1S,y1S,z1S,x2S,y2S,z2S = val:match(
'^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+) '..
'(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$')
-- box (2 vectors)
if x1S then return {
vector{tonumber(x1S),tonumber(y1S),tonumber(z1S)},
vector{tonumber(x2S),tonumber(y2S),tonumber(z2S)} } end
-- vector, box, or axis->matrix
local vec = multinumericFromTs(val)
if vec then return vec end
-- string
return val
end
local function arglistFromTs(name, argsS)
local args = {}
for i,arg in ipairs(argsS) do
for i,arg in ipairsNilable(argsS) do
args[i] = valFromTs(arg, name..':'..i)
end
return args
end
local function arglistToTs(args)
return map(args, valToTs)
local argsS = {}
for i,v in ipairsNilable(args) do
table.insert(argsS, valToTs(v))
end
return argsS
end
function bl.type(name,typ)
if typ~='bool' and typ~='boolean' and typ~='object' and typ~=nil then
error('bl.type: can only set type to \'bool\' or \'object\' or nil', 2) end
if not isValidFuncNameNsArgn(name) then
error('bl.type: invalid function or variable name \''..name..'\'', 2) end
if typ=='bool' then typ='boolean' end
bl._forceType[name:lower()] = typ
local function classFromForceTypeStr(name)
local class, rest = name:match('^([a-zA-Z0-9_]+)(::.+)$')
if not class then
class, rest = name:match('^([a-zA-Z0-9_]+)(%..+)$') end
return class,rest
end
local setForceType
setForceType = function(ftname, typ)
if typ~=nil and not fromTsForceTypes[typ] then
error('bl.type: invalid type \''..typ..'\'', 2) end
if not isValidFuncNameNsArgn(ftname) then
error('bl.type: invalid function or variable name \''..ftname..'\'', 2) end
ftname = ftname:lower()
bl._forceType[ftname] = typ
-- apply to child classes if present
local cname, rest = classFromForceTypeStr(ftname)
if cname then
local meta = bl._objectUserMetas[cname]
if meta then
for chcname,_ in pairs(meta._children) do
setForceType(chcname..rest, typ)
end
end
end
end
bl.type = setForceType
-- Type conversion TS->Lua backwards, convert back to string
local function numFromTsTostring(val)
-- todo: as-good-as-possible scientific notation for numbers
return tostring(val)
end
local function valFromTsTostring(val)
if type(val)=='string' then
return val
elseif type(val)=='number' then
return numFromTsTostring(val)
elseif type(val)=='table' then
if val.__is_vector then
local strs = {}
for i=1,#val do strs[i] = numFromTsTostring(val[i]) end
return table.concat(strs, ' ')
elseif val.__is_matrix then
-- todo: matrix back to axis-angle string
error('bl.string: matrix not yet supported', 3)
end
end
error('bl.string: cannot convert \''..type(val)..'\'', 3)
end
-- Value detection
-- Getting info from TS about functions and objects
local function isTsObject(t)
return type(t)=='table' and t._tsObjectId~=nil
end
local function tsIsObject(name) return tsBool(_bllua_ts.call('isObject', name)) end
local function tsIsFunction(name) return tsBool(_bllua_ts.call('isFunction', name)) end
local function tsIsFunctionNs(ns, name) return tsBool(_bllua_ts.call('isFunction', ns, name)) end
@@ -175,55 +276,78 @@ local function tsIsFunctionNsname(nsname)
if ns then return tsIsFunctionNs(ns, name)
else return tsIsFunction(nsname) end
end
-- sanity check to make sure objects that don't isObject are always marked as ._deleted
-- can be removed later
local function assertTsObjectExists(obj)
local is = tsIsObject(obj._tsObjectId)
if not is then
print('Warning: TS object :exists or isobject from lua but no longer exists') end
return is
end
function bl.isObject(obj)
if isTsObject(obj) then
obj = obj._tsObjectId
elseif type(obj)=='number' then
obj = tostring(obj)
elseif type(obj)~='string' then
if type(obj)=='number' then -- object id
return tsIsObject(tostring(obj))
elseif type(obj)=='string' then -- object name
return tsIsObject(obj)
elseif isTsObject(obj) then -- lua object
if obj._deleted then
return false
else
return assertTsObjectExists(obj)
end
else
error('bl.isObject: argument #1: expected torque object, number, or string', 2)
end
return tsIsObject(obj)
end
function bl.isFunction(a1, a2)
if type(a1)~='string' then
error('bl.isFunction: argument #1: expected string', 2) end
if a2 then
if type(a2)~='string' then
error('bl.isFunction: argument #2: expected string', 2) end
return tsIsFunctionNs(a1, a2)
else
return tsIsFunction(a1)
end
function bl.isFunction(name)
return tsIsFunctionNsname(name)
end
-- Torque object pseudo-class
local tsClassMeta = {
__tostring = function(t)
return 'torqueClass:'..t._name..
(t._inherit and (':'..t._inherit._name) or '')
end,
}
bl._objectUserMetas = bl._objectUserMetas or {}
function bl.class(name, inherit)
if not ( type(name)=='string' and isValidFuncName(name) ) then
function bl.class(cname, inhname)
if not ( type(cname)=='string' and isValidFuncName(cname) ) then
error('bl.class: argument #1: invalid class name', 2) end
if not ( inherit==nil or (type(inherit)=='string' and isValidFuncName(inherit)) ) then
if not ( inhname==nil or (type(inhname)=='string' and isValidFuncName(inhname)) ) then
error('bl.class: argument #2: inherit name must be a string or nil', 2) end
name = name:lower()
cname = cname:lower()
local met = bl._objectUserMetas[name] or {}
bl._objectUserMetas[name] = met
met._name = name
local met = bl._objectUserMetas[cname] or {
_name = cname,
_inherit = nil,
_children = {},
}
bl._objectUserMetas[cname] = met
setmetatable(met, tsClassMeta)
if inherit then
inherit = inherit:lower()
if inhname then
inhname = inhname:lower()
local inh = bl._objectUserMetas[inherit]
if not inh then error('bl.class: argument #2: \''..inherit..'\' is not the '..
local inh = bl._objectUserMetas[inhname]
if not inh then error('bl.class: argument #2: \''..inhname..'\' is not the '..
'name of an existing class', 2) end
local inhI = bl._objectUserMetas[name]._inherit
inh._children[cname] = true
local inhI = met._inherit
if inhI and inhI~=inh then
error('bl.class: argument #2: class already exists and '..
'inherits a different parent.', 2) end
met._inherit = inh
bl._objectUserMetas[name]._inherit = inh
-- apply inherited method and field types
for ftname, typ in pairs(bl._forceType) do
local cname2, rest = classFromForceTypeStr(ftname)
if cname2==inhname then
setForceType(cname..rest, typ)
end
end
end
end
local function objectInheritedMetas(name)
@@ -255,15 +379,24 @@ local tsObjectMeta = {
for inh in objectInheritedMetas(rawget(t,'_tsClassName')) do
if inh[name] then return inh[name] end
end
if tsIsFunctionNs(rawget(t,'_tsNamespace'), name) then
return function(t, ...)
local args = {...}
local argsS = arglistToTs(args)
return valFromTs(_bllua_ts.callobj(rawget(t,'_tsObjectId'), name, unpack(argsS)),
rawget(t,'_tsNamespace')..'::'..name)
if
tsIsFunctionNs(rawget(t,'_tsNamespace'), name) or
tsIsFunctionNs(rawget(t,'_tsName'), name)
then
return function(t2, ...)
if t2==nil or type(t2)~='table' or not t2._tsObjectId then
error('ts object method: be sure to use :func() not .func()', 2) end
local argsS = arglistToTs({...})
local res =
_bllua_ts.callobj(t2._tsObjectId, name, unpack(argsS))
return valFromTs(res,
t2._tsName and t2._tsName..'::'..name,
t2._tsNamespace..'::'..name)
end
else
return valFromTs(_bllua_ts.getfield(rawget(t,'_tsObjectId'), name),
local res = _bllua_ts.getfield(rawget(t,'_tsObjectId'), name)
return valFromTs(res,
rawget(t,'_tsName') and rawget(t,'_tsName')..'.'..name,
rawget(t,'_tsNamespace')..'.'..name)
end
end
@@ -295,7 +428,8 @@ local tsObjectMeta = {
-- Display a nice info string
__tostring = function(t)
return 'torque:'..t._tsNamespace..':'..t._tsObjectId..
(t._tsName~='' and ('('..t._tsName..')') or '')
(t._tsName~='' and ('('..t._tsName..')') or '')..
(t._deleted and '(deleted)' or '')
end,
-- #object
-- If the object has a getCount method, return its count
@@ -326,7 +460,8 @@ local tsObjectMeta = {
local obj = toTsObject(_bllua_ts.callobj(t._tsObjectId,
'getObject', tostring(idx)))
idx = idx+1
return idx-1, obj
--return idx-1, obj
return obj
else
return nil
end
@@ -375,8 +510,10 @@ local tsObjectMeta = {
if t==nil then
error('ts object method: be sure to use :func() not .func()', 2) end
if t._deleted then
return false end
return tsIsObject(t._tsObjectId)
return false
else
return assertTsObjectExists(t)
end
end,
}
-- Weak-values table for caching Torque object references
@@ -409,7 +546,7 @@ toTsObject = function(idiS)
idiS = idiS:lower()
if bl._objectsWeak[idiS] then return bl._objectsWeak[idiS] end
if not tsBool(_bllua_ts.call('isObject', idiS)) then
if not tsIsObject(idiS) then
--error('toTsObject: object \''..idiS..'\' does not exist', 2) end
return nil end
@@ -418,7 +555,7 @@ toTsObject = function(idiS)
_tsObjectId = _bllua_ts.callobj(idiS, 'getId' ),
_tsName = _bllua_ts.callobj(idiS, 'getName' ),
_tsNamespace = className,
_tsClassName = className:lower()
_tsClassName = className:lower(),
}
setmetatable(obj, tsObjectMeta)
@@ -427,6 +564,30 @@ toTsObject = function(idiS)
return obj
end
-- Allow bl['namespaced::function']()
local function safeNamespaceName(name)
return tostring(name:gsub(':', '_'))
end
bl._cachedNamespaceCalls = {}
local function tsNamespacedCallTfname(name)
local tfname = bl._cachedNamespaceCalls[name]
if not tfname then
tfname = '_bllua_nscall_'..safeNamespaceName(name)
local tfcode = 'function '..tfname..'('..tsArgsLocal..'){'..
name..'('..tsArgsLocal..');}'
_bllua_ts.eval(tfcode)
bl._cachedNamespaceCalls[name] = tfname
end
return tfname
end
local function tsCallGen(name)
return function(...)
local argsS = arglistToTs({...})
local res = _bllua_ts.call(name, unpack(argsS))
return valFromTs(res, name)
end
end
-- Metatable for the global bl library
-- Allows accessing Torque objects, variables, and functions by indexing it
local tsMeta = {
@@ -438,7 +599,7 @@ local tsMeta = {
__index = function(t, name)
if getmetatable(t)[name] then
return getmetatable(t)[name]
elseif bl._objectUserMetas[name:lower()] then
elseif type(name)=='string' and bl._objectUserMetas[name:lower()] then
return bl._objectUserMetas[name:lower()]
else
if type(name)=='number' then
@@ -446,21 +607,19 @@ local tsMeta = {
elseif name:find('::') then
local ns, rest = name:match('^([^:]+)::(.+)$')
if not ns then error('ts index: invalid name \''..name..'\'', 2) end
if not rest:find('::') and tsIsFunction(ns, rest) then
error('ts index: can\'t call a namespaced function from lua', 2)
if not rest:find('::') and tsIsFunctionNs(ns, rest) then
return tsCallGen(tsNamespacedCallTfname(name))
else
return valFromTs(_bllua_ts.getvar(name), name)
local res = _bllua_ts.getvar(name)
return valFromTs(res, name)
end
elseif tsIsFunction(name) then
return function(...)
local args = {...}
local argsS = arglistToTs(args)
return valFromTs(_bllua_ts.call(name, unpack(argsS)), name)
end
return tsCallGen(name)
elseif tsIsObject(name) then
return toTsObject(name)
else
return valFromTs(_bllua_ts.getvar(name), name)
local res = _bllua_ts.getvar(name)
return valFromTs(res, name)
end
end
end,
@@ -469,7 +628,7 @@ local tsMeta = {
-- bl.set(name, value)
-- Used to set global variables
function bl.set(name, val)
_bllua_ts.call('_bllua_set_var', name, valToTs(val))
_bllua_ts.setvar(name, valToTs(val))
end
-- Utility functions
@@ -479,34 +638,90 @@ function bl.call(func, ...)
return _bllua_ts.call(func, unpack(argsS))
end
function bl.eval(code)
return valFromTs(_bllua_ts.call('eval', code))
local res = _bllua_ts.eval(code)
return valFromTs(res)
end
function bl.exec(file)
return valFromTs(_bllua_ts.call('exec', file))
local res = _bllua_ts.call('exec', file)
return valFromTs(res)
end
function bl.tobool(val)
function bl.array(name, ...)
local rest = {...}
return name..table.concat(rest, '_')
end
function bl.boolean(val)
return val~=nil and
val~=false and
--val~='' and
--val~='0' and
val~=0
end
function bl.toobject(id)
function bl.object(id)
if type(id)=='table' and id._tsObjectId then
return id
elseif type(id)=='string' or type(id)=='number' then
return toTsObject(tostring(id))
else
error('bl.toobject: id must be a ts object, number, or string', 2)
error('bl.object: id must be a ts object, number, or string', 2)
end
end
function bl.array(name, ...)
local rest = {...}
return name..table.concat(rest, '_')
function bl.string(val)
return valFromTsTostring(val)
end
function _bllua_call(name, ...)
-- todo: call ts->lua using this instead of directly
-- Lua calling from TS
local luaLookup
luaLookup = function(tbl, name, set, val)
if name:find('%.') then
local first, rest = name:match('^([^%.:]+)%.(.+)$')
if not isValidFuncName(first) then
error('luaLookup: invalid name \''..tostring(first)..'\'', 3) end
if not tbl[first] then
if set then tbl[first] = {}
else return nil end
end
return luaLookup(tbl[first], rest, set, val)
elseif name:find(':') then
local first, rest = name:match('^([^%.:]+):(.*)$')
if rest:find('[%.:]') then
error('luacall: cannot have : or . after :', 3) end
if not isValidFuncName(first) then
error('luacall: invalid name \''..tostring(first)..'\'', 3) end
if not isValidFuncName(rest) then
error('luacall: invalid method name \''..tostring(first)..'\'', 3) end
if not tbl[first] then
error('luacall: no object named \''..rest..'\'', 3) end
if not tbl[first][rest] then
error('luacall: no method named \''..rest..'\'', 3) end
return function(...)
tbl[first][rest](tbl[first], ...)
end
else
if set then
tbl[name] = val
else
return tbl[name]
end
end
end
-- Todo: similar deep access for luaget and luaset
function _bllua_call(fname, ...)
local args = arglistFromTs(fname:lower(), {...}) -- todo: separate lua from ts func names?
local func = luaLookup(_G, fname)
if not func then
error('luacall: no global in lua named \''..fname..'\'', 2) end
local res = func(unpack(args))
return valToTs(res)
end
function _bllua_getvar(vname)
return valToTs(luaLookup(_G, vname))
end
function _bllua_setvar(vname, valS)
return valToTs(luaLookup(_G, vname, true, valFromTs(valS, vname))) -- todo: separate lua from ts var names?
end
function _bllua_eval(code) return loadstring(code)() end
function _bllua_exec(fn) return dofile(fn, 2) end
-- bl.schedule: Use TS's schedule function to schedule lua calls
-- bl.schedule(time, function, args...)
@@ -524,7 +739,7 @@ function bl.schedule(time, cb, ...)
bl._scheduleNextId = bl._scheduleNextId+1
local args = {...}
local handle = tonumber(_bllua_ts.call('schedule',
time, 0, 'luacall', '_bllua_schedule_callback', id))
time, 0, '_bllua_luacall', '_bllua_schedule_callback', id))
local sch = {
callback = cb,
args = args,
@@ -534,35 +749,57 @@ function bl.schedule(time, cb, ...)
bl._scheduleTable[id] = sch
return sch
end
function _bllua_schedule_callback(id)
id = tonumber(id)
function _bllua_schedule_callback(idS)
local id = tonumber(idS) or
error('_bllua_schedule_callback: invalid id: '..tostring(idS))
local sch = bl._scheduleTable[id]
if not sch then error('_ts_schedule_callback: no schedule with id '..id) end
if not sch then error('_bllua_schedule_callback: no schedule with id '..id) end
bl._scheduleTable[id] = nil
sch.callback(unpack(sch.args))
end
-- serverCmd and clientCmd
-- bl.serverCmd('suicide', function(client) client.player:kill() end)
bl._cmds = bl._cmds or {}
function _bllua_process_cmd(cmdS, clientS, ...)
local client = toTsObject(clientS)
local argsS = {...}
local args = arglistFromTs(cmdS, argsS)
local func = bl._cmds[cmdS]
function _bllua_process_cmd(cmdS, ...)
local cmd = cmdS:lower()
local args = arglistFromTs(cmd, {...})
local func = bl._cmds[cmd]
if not func then error('_bllua_process_cmd: no cmd named \''..cmd..'\'') end
pcall(func, client, unpack(args))
func(unpack(args)) --pcall(func, unpack(args))
end
local function addCmd(cmd, func)
if not isValidFuncName(cmd) then
error('addCmd: invalid function name \''..tostring(cmd)..'\'') end
bl._servercmds[cmd] = func
local arglist = '%a,%b,%c,%d,%e,%f,%g,%h'
_bllua_ts.eval('function '..cmd..'(%cl,'..arglist..'){'..
'luacall(_bllua_process_cmd,"'..cmd..'",%cl,'..arglist..');}')
bl._cmds[cmd] = func
_bllua_ts.eval('function '..cmd..'('..tsArgsLocal..'){'..
'_bllua_luacall(_bllua_process_cmd,"'..cmd..'",'..tsArgsLocal..');}')
end
function bl.addServerCmd(name, func)
name = name:lower()
addCmd('servercmd'..name, func)
bl._forceType['servercmd'..name..':1'] = 'object'
end
function bl.addClientCmd(name, func)
name = name:lower()
addCmd('clientcmd'..name, func)
end
-- commandToServer and commandToClient
function bl.commandToServer(cmd, ...)
_bllua_ts.call('commandToServer',
_bllua_ts.call('addTaggedString', cmd),
unpack(arglistToTs({...})))
end
function bl.commandToClient(cmd, ...)
_bllua_ts.call('commandToClient',
_bllua_ts.call('addTaggedString', cmd),
unpack(arglistToTs({...})))
end
function bl.commandToAll(cmd, ...)
_bllua_ts.call('commandToAll',
_bllua_ts.call('addTaggedString', cmd),
unpack(arglistToTs({...})))
end
function bl.serverCmd(name, func) addCmd('serverCmd'..name, func) end
function bl.clientCmd(name, func) addCmd('clientCmd'..name, func) end
-- Hooks (using TS packages)
local function isPackageActive(pkg)
@@ -584,41 +821,64 @@ local function deactivatePackage(pkg)
end
end
bl._hooks = bl._hooks or {}
function _bllua_process_hook(pkgS, nameS, timeS, ...)
local argsS = {...}
local args = arglistFromTs(nameS, argsS)
function _bllua_process_hook_before(pkgS, nameS, ...)
local args = arglistFromTs(nameS, {...})
local func = bl._hooks[pkgS] and bl._hooks[pkgS][nameS] and
bl._hooks[pkgS][nameS][timeS]
bl._hooks[pkgS][nameS].before
if not func then
error('_bllua_process_hook: no hook for '..pkgS..':'..nameS..':'..timeS) end
pcall(func, args)
error('_bllua_process_hook_before: no hook for '..pkgs..':'..nameS) end
_bllua_ts.setvar('_bllua_hook_abort', '0')
func(args) --pcall(func, args)
if args._return then
_bllua_ts.setvar('_bllua_hook_abort', '1')
_bllua_ts.setvar('_bllua_hook_return', valToTs(args._return))
end
for i=1,tsMaxArgs do
_bllua_ts.setvar('_bllua_hook_arg'..i, valToTs(args[i]))
end
end
function _bllua_process_hook_after(pkgS, nameS, resultS, ...)
nameS = nameS:lower()
local args = arglistFromTs(nameS, {...})
args._return = valFromTs(resultS, nameS)
local func = bl._hooks[pkgS] and bl._hooks[pkgS][nameS] and
bl._hooks[pkgS][nameS].after
if not func then
error('_bllua_process_hook_after: no hook for '..pkgs..':'..nameS) end
func(args) --pcall(func, args)
return valToTs(args._return)
end
local function updateHook(pkg, name, hk)
local arglist = '%a,%b,%c,%d,%e,%f,%g,%h'
local beforeCode = hk.before and
('luacall("_bllua_process_hook", "'..pkg..'", "'..name..
'", "before", '..arglist..');') or ''
local parentCode = hk.override and
('luacall("_bllua_process_hook", "'..pkg..'", "'..name..
'", "override", '..arglist..');') or
(tsIsFunctionNsname(name) and
('parent::'..name:match('[^:]+$')..'('..arglist..');') or '')
('_bllua_luacall("_bllua_process_hook_before","'..pkg..'","'..name..
'",'..tsArgsLocal..');') or ''
local arglist = (hk.before and tsArgsGlobal or tsArgsLocal)
local parentCode =
tsIsFunctionNsname(name) and -- only call parent if it exists
(hk.before and
'if($_bllua_hook_abort)return $_bllua_hook_return;else ' or '')..
((hk.after and '%result=' or 'return ')..
'parent::'..name:match('[^:]+$')..
'('..arglist..');') or ''
local afterCode = hk.after and
('luacall("_bllua_process_hook", "'..pkg..'", "'..name..
'", "after", '..arglist..');') or ''
bl.eval('package '..pkg..'{function '..name..'('..arglist..'){'..
beforeCode..parentCode..afterCode..'}};')
('return _bllua_luacall("_bllua_process_hook_after","'..pkg..'","'..name..'",%result,'..
arglist..');') or ''
local code =
'package '..pkg..'{'..
'function '..name..'('..tsArgsLocal..'){'..
beforeCode..parentCode..afterCode..
'}'..
'};'
print('bl.hook eval output: [['..code..']]')
_bllua_ts.eval(code)
end
function bl.hook(pkg, name, time, func)
if not isValidFuncName(pkg) then
error('bl.hook: argument #1: invalid package name \''..tostring(pkg)..'\'', 2) end
if not isValidFuncNameNs(name) then
error('bl.hook: argument #2: invalid function name \''..tostring(name)..'\'', 2) end
if time~='before' and time~='after' and time~='override' then
error('bl.hook: argument #3: time must be one of '..
'\'before\' \'after\' \'override\'', 2) end
if time~='before' and time~='after' then
error('bl.hook: argument #3: time must be \'before\' or \'after\'', 2) end
if type(func)~='function' then
error('bl.hook: argument #4: expected a function', 2) end
@@ -629,14 +889,16 @@ function bl.hook(pkg, name, time, func)
updateHook(pkg, name, bl._hooks[pkg][name])
activatePackage(pkg)
end
local function tableEmpty(t)
return next(t)~=nil
end
function bl.unhook(pkg, name, time)
if not isValidFuncName(pkg) then
error('bl.unhook: argument #1: invalid package name \''..tostring(pkg)..'\'', 2) end
if not isValidFuncNameNs(name) then
if name and not isValidFuncNameNs(name) then
error('bl.unhook: argument #2: invalid function name \''..tostring(name)..'\'', 2) end
if time~='before' and time~='after' and time~='override' then
error('bl.unhook: argument #3: time must be one of '..
'\'before\' \'after\' \'override\'', 2) end
if time and time~='before' and time~='after' then
error('bl.unhook: argument #3: time must be \'before\' or \'after\'', 2) end
if not name then
if bl._hooks[pkg] then
@@ -653,21 +915,25 @@ function bl.unhook(pkg, name, time)
if bl._hooks[pkg][name] then
if not time then
bl._hooks[pkg][name] = nil
if table.isempty(bl._hooks[pkg]) then
if table.empty(bl._hooks[pkg]) then
bl._hooks[pkg] = nil
deactivatePackage(pkg)
end
updateHook(pkg, name, {})
else
if time~='before' and time~='after' and time~='override' then
error('bl.unhook: argument #3: time must be nil or one of '..
'\'before\' \'after\' \'override\'', 2) end
if time~='before' and time~='after' then
error('bl.unhook: argument #3: time must be nil, \'before\', or \'after\'', 2) end
bl._hooks[pkg][name][time] = nil
if table.isempty(bl._hooks[pkg][name]) and table.empty(bl._hooks[pkg]) then
bl._hooks[pkg] = nil
deactivatePackage(pkg)
if tableEmpty(bl._hooks[pkg][name]) then
bl._hooks[pkg][name] = nil
end
if tableEmpty(bl._hooks[pkg]) then
bl._hooks[pkg] = nil
updateHook(pkg, name, {})
deactivatePackage(pkg)
else
updateHook(pkg, name, bl._hooks[pkg][name])
end
updateHook(pkg, name, bl._hooks[pkg][name])
end
else
--error('bl.unhook: no hooks registered for function \''..name..
@@ -720,7 +986,7 @@ function bl.raycast(start, stop, mask, ignores)
local stopS = vecToTs(start)
local maskS = maskToTs(mask)
local ignoresS = {}
for _,v in ipairs(ignores) do
for _,v in ipairsNilable(ignores) do
table.insert(ignoresS, objToTs(v))
end
@@ -766,10 +1032,15 @@ function bl.radiusSearch(pos, radius, mask)
end
-- Print/Talk/Echo
local maxTsArgLen = 8192
local function valsToString(vals)
local strs = {}
for i,v in ipairs(vals) do
strs[i] = table.tostring(v)
for i,v in ipairsNilable(vals) do
local tstr = table.tostring(v)
if #tstr>maxTsArgLen then
tstr = tostring(v)
end
strs[i] = tstr
end
return table.concat(strs, ' ')
end
@@ -784,12 +1055,15 @@ bl.talk = function(...)
_bllua_ts.call('talk', str)
end
-- bl.new and bl.datablock
local function createTsObj(keyword, class, name, inherit, props)
local propsT = {}
for k,v in pairs(props) do
if not isValidFuncName(k) then
error('bl.new/datablock: invalid property name \''..k..'\'') end
table.insert(propsT, k..'="'..valToTs(v)..'";')
if props then
for k,v in pairs(props) do
if not isValidFuncName(k) then
error('bl.new/bl.datablock: invalid property name \''..k..'\'') end
table.insert(propsT, k..'="'..valToTs(v)..'";')
end
end
local objS = _bllua_ts.eval(
@@ -798,7 +1072,7 @@ local function createTsObj(keyword, class, name, inherit, props)
table.concat(propsT)..'};')
local obj = toTsObject(objS)
if not obj then
error('bl.new/datablock: failed to create object', 3) end
error('bl.new/bl.datablock: failed to create object', 3) end
return obj
end
@@ -821,7 +1095,7 @@ local function parseTsDecl(decl)
isValidFuncName(class) and
(name==nil or isValidFuncName(name)) and
(inherit==nil or isValidFuncName(inherit)) ) then
error('bl.new/datablock: invalid decl \''..decl..'\'\n'..
error('bl.new/bl.datablock: invalid decl \''..decl..'\'\n'..
'must be of the format: \'className\', \'className name\', '..
'\'className :inherit\', or \'className name:inherit\'', 3) end
return class, name, inherit
@@ -832,6 +1106,7 @@ function bl.new(decl, props)
end
function bl.datablock(decl, props)
local class, name, inherit = parseTsDecl(decl)
if not name then error('bl.datablock: must specify a name', 2) end
return createTsObj('datablock', class, name, inherit, props)
end

View File

@@ -89,11 +89,12 @@ local allowed_zip_dirs = tflip{
local function io_open_absolute(fn, mode)
-- if file exists, use original mode
local res, err = _bllua_io_open(fn, mode)
if res then return res end
if res then return res
elseif err and not err:find('No such file or directory$') then return nil, err end
-- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader
local dir = fn:match('^[^/]+')
if not allowed_zip_dirs[dir:lower()] then return nil, 'File is not in one of the allowed directories' end
if not allowed_zip_dirs[dir:lower()] then return nil, 'Zip is not in one of the allowed directories' end
local exist = _bllua_ts.call('isFile', fn) == '1'
if not exist then return nil, err end
@@ -116,13 +117,13 @@ function io.open(fn, mode, errn)
local relfn = curfn and fn:find('^%./') and
curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '')
if relfn then
local fi, err = io_open_absolute(relfn, mode, errn+1)
local fi, err = io_open_absolute(relfn, mode)
return fi, err, relfn
else
return nil, 'Invalid path', fn
end
else
local fi, err = io_open_absolute(fn, mode, errn+1)
local fi, err = io_open_absolute(fn, mode)
return fi, err, fn
end
end
@@ -192,12 +193,6 @@ function require(mod)
return _bllua_requiresecure(mod)
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
@@ -213,4 +208,8 @@ function _bllua_smarteval(code)
end
end
_bllua_ts.call('echo', ' Executed libts.lua')
function ts.setvar(name, val)
_bllua_ts.call('_bllua_set_var', name, val)
end
_bllua_ts.call('echo', ' Executed libts-lua.lua')

View File

@@ -49,4 +49,4 @@ function _bllua_set_var(%name, %val) {
return "";
}
echo(" Executed libts-support.cs");
echo(" Executed libts-ts.cs");

7
src/util/matrix.lua Normal file
View File

@@ -0,0 +1,7 @@
-- todo
-- Matrix class with math operators
print(' Executed matrix.lua')

View File

@@ -5,8 +5,7 @@
-- Table / List
-- Whether a table contains no keys
function table.empty(t)
for _,_ in pairs(t) do return false end
return true
return next(t)~=nil
end
-- Apply a function to each key in a table
function table.map(f, ...)
@@ -14,8 +13,18 @@ function table.map(f, ...)
local u = {}
for k,_ in pairs(ts[1]) do
local args = {}
for j=1,#ts do args[j] = ts[j][i] end
u[i] = f(unpack(args))
for j=1,#ts do args[j] = ts[j][k] end
u[k] = f(unpack(args))
end
return u
end
function table.mapk(f, ...)
local ts = {...}
local u = {}
for k,_ in pairs(ts[1]) do
local args = {}
for j=1,#ts do args[j] = ts[j][k] end
u[k] = f(k, unpack(args))
end
return u
end
@@ -29,6 +38,16 @@ function table.map_list(f, ...)
end
return u
end
function table.mapi_list(f, ...)
local ts = {...}
local u = {}
for i=1,#ts[1] do
local args = {}
for j=1,#ts do args[j] = ts[j][i] end
u[i] = f(i, unpack(args))
end
return u
end
-- Swap keys/values
function table.swap(t)
local u = {}
@@ -41,18 +60,6 @@ function table.reverse(l)
for i=1,#l do m[#l-i+1] = l[i] end
return m
end
-- Convert i->v to v->true
function table.values(l)
local u = {}
for _,v in ipairs(l) do u[v] = true end
return u
end
-- Make a list of keys
function table.keys(t)
local u = {}
for k,_ in pairs(t) do table.insert(u, k) end
return u
end
-- Whether a table is a list/array (has only monotonic integer keys)
function table.islist(t)
local n = 0
@@ -192,7 +199,7 @@ valueToString = function(v, tabLevel, seen)
return tostring(v)
else
--error('table.tostring: table contains a '..t..' value, cannot serialize')
return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]'
return 'nil --[[ '..tostring(v)..' ]]'
end
end
function table.tostring(t)
@@ -244,8 +251,8 @@ function string.bytes(s)
end
-- Trim leading and trailing whitespace
function string.trim(s, ws)
ws = ws or '[ \t\r\t]'
return s:gsub('^'..ws..'+', ''):gsub(ws..'+$', '')..''
ws = ws or ' \t\r\n'
return s:gsub('^['..ws..']+', ''):gsub('['..ws..']+$', '')..''
end
-- String slicing and searching using [] operator
local str_meta = getmetatable('')
@@ -325,7 +332,7 @@ end
io = io or {}
-- Read entire file at once, return nil,err if access failed
function io.readfile(filename)
function io.readall(filename)
local fi,err = io.open(filename, 'rb')
if not fi then return nil,err end
local s = fi:read("*a")
@@ -333,7 +340,7 @@ function io.readfile(filename)
return s
end
-- Write data to file all at once, return true if success / false,err if failure
function io.writefile(filename, data)
function io.writeall(filename, data)
local fi,err = io.open(filename, 'wb')
if not fi then return false,err end
fi:write(data)
@@ -358,3 +365,6 @@ end
function math.clamp(v, n, x)
return math.min(x, math.max(v, n))
end
print(' Executed std.lua')

View File

@@ -127,7 +127,7 @@ local vector_meta = {
for i = 1, len do
table.insert(st, tostring(v1[i]))
end
return '{ '..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),
@@ -162,10 +162,10 @@ local vector_meta = {
else error('vector rotateByAngleId: invalid rotation '..r, 2) end
return v2
end,
rotate2d = function(v, r)
rotateZ = function(v, r)
--vector_check(v, 2, 'rotate2d')
if type(r)~='number' then
error('vector rotate2d: invalid rotation '..tostring(r), 2) end
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 }
@@ -216,4 +216,7 @@ vector_new = function(vi)
end
vector = vector_new
print(' Executed vector.lua')
return vector_new