1
0
forked from redo/BlockLua

Merge branch 'master'

This commit is contained in:
2025-10-06 13:11:57 -04:00
12 changed files with 1339 additions and 1331 deletions

BIN
BlockLua.dll Normal file

Binary file not shown.

View File

@@ -36,6 +36,8 @@ Lua scripting for Blockland
`object:method(args)` - Call a Torque object method `object:method(args)` - Call a Torque object method
`object[index]` - Access a member of a Torque set or group `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 childIndex, 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
### Timing/Schedules ### Timing/Schedules
`sched = bl.schedule(timeMs, function, args...)` - Schedule a Lua function to be called later, similar to schedule in Torque `sched = bl.schedule(timeMs, function, args...)` - Schedule a Lua function to be called later, similar to schedule in Torque
@@ -101,10 +103,11 @@ When reading from outside ZIPs, binary files are fully supported.
`bl.type('className::funcName', 'type')` - Register the return type of a Torque object method. `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')` - 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.class('className', 'parentClassName')` - Same as above, with inheritance
`bl.boolean(thing)` - Manually convert a Torque boolean (0 or 1) into a Lua boolean. `bl.boolean(arg)` - Manually convert a Torque boolean (0 or 1) into a Lua boolean.
`bl.object(thing)` - Manually convert a Torque object reference (object ID or name) into a Lua object. `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.
### Vectors ### Vector
`vec = vector{x,y,z}` - Create a vector. Can have any number of elements `vec = vector{x,y,z}` - Create a vector. Can have any number of elements
`vec1 + vec2` - Add `vec1 + vec2` - Add
`vec1 - vec2` - Subtract `vec1 - vec2` - Subtract
@@ -129,6 +132,9 @@ When reading from outside ZIPs, binary files are fully supported.
`vec1:distance(vec2)` - Distance between two points `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. `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 ### Extended Standard Lua Library
`string[index]` `string[index]`
`string[{start,stop}]` `string[{start,stop}]`
@@ -163,12 +169,7 @@ When reading from outside ZIPs, binary files are fully supported.
## Type Conversion ## 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. 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. 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 `bl.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 ### From Lua to TorqueScript
- `nil` becomes the empty string "" - `nil` becomes the empty string ""
- `true` and `false` become "1" and "0" respectively - `true` and `false` become "1" and "0" respectively
@@ -178,6 +179,17 @@ TorqueScript stores no type information; all values in TorqueScript are strings.
- Any `string` is passed directly as a string - Any `string` is passed directly as a string
- Tables cannot be passed and will throw an error - Tables cannot be passed and will throw an error
### From TorqueScript to Lua
- 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.
- The empty string "" becomes `nil`
- 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" in TorqueScript parlance), 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 objects by hand, use `bl.object`, `bl.boolean`, or `bl.string`.
## I/O and Safety ## I/O and Safety
All Lua code is sandboxed, and file access is confined 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. BlockLua also has access to any C libraries installed in the `modules/lualib` folder, so be careful throwing things in there.

View File

@@ -31,6 +31,7 @@ INCLUDE_BIN(bll_fileLuaEnv, "lua-env.lua");
INCLUDE_BIN(bll_fileTsEnv, "ts-env.cs"); INCLUDE_BIN(bll_fileTsEnv, "ts-env.cs");
INCLUDE_BIN(bll_fileLuaStd, "util/std.lua"); INCLUDE_BIN(bll_fileLuaStd, "util/std.lua");
INCLUDE_BIN(bll_fileLuaVector, "util/vector.lua"); INCLUDE_BIN(bll_fileLuaVector, "util/vector.lua");
INCLUDE_BIN(bll_fileLuaMatrix, "util/matrix.lua");
INCLUDE_BIN(bll_fileLuaLibts, "util/libts-lua.lua"); INCLUDE_BIN(bll_fileLuaLibts, "util/libts-lua.lua");
INCLUDE_BIN(bll_fileTsLibts, "util/libts-ts.cs"); INCLUDE_BIN(bll_fileTsLibts, "util/libts-ts.cs");
INCLUDE_BIN(bll_fileLuaLibbl, "util/libbl.lua"); INCLUDE_BIN(bll_fileLuaLibbl, "util/libbl.lua");
@@ -72,6 +73,7 @@ bool init() {
// Load utilities // Load utilities
BLL_LOAD_LUA(gL, bll_fileLuaStd); BLL_LOAD_LUA(gL, bll_fileLuaStd);
BLL_LOAD_LUA(gL, bll_fileLuaVector); BLL_LOAD_LUA(gL, bll_fileLuaVector);
BLL_LOAD_LUA(gL, bll_fileLuaMatrix);
BLL_LOAD_LUA(gL, bll_fileLuaLibts); BLL_LOAD_LUA(gL, bll_fileLuaLibts);
BlEval(bll_fileTsLibts); BlEval(bll_fileTsLibts);
BLL_LOAD_LUA(gL, bll_fileLuaLibbl); BLL_LOAD_LUA(gL, bll_fileLuaLibbl);

View File

@@ -143,4 +143,5 @@ debug = {
getfilename = old_debug.getfilename, -- defined in lua.env.lua 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,5 @@ function _bllua_on_error(err)
return table.concat(tracelines, '\n') return table.concat(tracelines, '\n')
end end
_bllua_ts.echo(' Executed bllua-env.lua') print = _bllua_ts.echo
print(' Executed bllua-env.lua')

View File

@@ -42,4 +42,26 @@ package _bllua_objectDeletionHook {
}; };
activatePackage(_bllua_objectDeletionHook); 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"); echo(" Executed libbl-support.cs");

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
-- This Lua code provides some built-in utilities for writing Lua add-ons -- This Lua code provides some built-in utilities for writing Lua add-ons
-- It is eval'd automatically once BLLua3 has loaded the TS API and environment -- It is eval'd automatically once BLLua3 has loaded the TS API and environment
-- It only has access to the sandboxed lua environment, just like user code. -- It only has access to the sandboxed lua environment, just like user code.
@@ -6,11 +7,8 @@ ts = _bllua_ts
-- Provide limited OS functions -- Provide limited OS functions
os = os or {} os = os or {}
---@diagnostic disable-next-line: duplicate-set-field function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime'))/1000) end
function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime')) / 1000) end function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end
---@diagnostic disable-next-line: duplicate-set-field
function os.clock() return tonumber(_bllua_ts.call('getSimTime')) / 1000 end
-- Virtual file class, emulating a file object as returned by io.open -- Virtual file class, emulating a file object as returned by io.open
-- Used to wrap io.open to allow reading from zips (using TS) -- Used to wrap io.open to allow reading from zips (using TS)
@@ -18,150 +16,140 @@ function os.clock() return tonumber(_bllua_ts.call('getSimTime')) / 1000 end
-- Can't read nulls, can't distinguish between CRLF and LF. -- Can't read nulls, can't distinguish between CRLF and LF.
-- Todo someday: actually read the zip in lua? -- Todo someday: actually read the zip in lua?
local file_meta = { local file_meta = {
read = function(file, mode) read = function(file, mode)
file:_init() file:_init()
if not file or type(file) ~= 'table' or not file._is_file then error('File:read: Not a file', 2) end if not file or type(file)~='table' or not file._is_file then error('File:read: Not a file', 2) end
if file._is_open ~= true then error('File:read: File is closed', 2) end if file._is_open ~= true then error('File:read: File is closed', 2) end
if mode == '*n' then if mode=='*n' then
local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos)
if n then if n then
file.pos = file.pos + #ws + #n file.pos = file.pos + #ws + #n
return n return n
else else
return nil return nil
end end
elseif mode == '*a' then elseif mode=='*a' then
local d = file.data:sub(file.pos, #file.data) local d = file.data:sub(file.pos, #file.data)
file.pos = #file.data + 1 file.pos = #file.data + 1
return d return d
elseif mode == '*l' then elseif mode=='*l' then
local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos)
if not l then if not l then
l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; l = file.data:match('^([^\r\n]*)$', file.pos); ws = '';
if l == '' then return nil end if l=='' then return nil end
end end
if l then if l then
file.pos = file.pos + #l + #ws file.pos = file.pos + #l + #ws
return l return l
else else
return nil return nil
end end
elseif type(mode) == 'number' then elseif type(mode)=='number' then
local d = file.data:sub(file.pos, file.pos + mode) local d = file.data:sub(file.pos, file.pos+mode)
file.pos = file.pos + #d file.pos = file.pos + #d
return d return d
else else
error('File:read: Invalid mode \'' .. mode .. '\'', 2) error('File:read: Invalid mode \''..mode..'\'', 2)
end end
end, end,
lines = function(file) lines = function(file)
file:_init() file:_init()
return function() return function()
return file:read('*l') return file:read('*l')
end end
end, end,
close = function(file) close = function(file)
if not file._is_open then error('File:close: File is not open', 2) end if not file._is_open then error('File:close: File is not open', 2) end
file._is_open = false file._is_open = false
end, end,
__index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end, __index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end,
_init = function(f) _init = function(f)
if not f.data then if not f.data then
f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename) f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename)
end end
end, end,
} }
local function new_file_obj(fn) local function new_file_obj(fn)
local file = { local file = {
_is_file = true, _is_file = true,
_is_open = true, _is_open = true,
pos = 1, pos = 1,
__index = file_meta.__index, __index = file_meta.__index,
filename = fn, filename = fn,
data = nil, data = nil,
} }
setmetatable(file, file_meta) setmetatable(file, file_meta)
return file return file
end end
local function tflip(t) local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end
local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; local allowed_zip_dirs = tflip{
end 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
local allowed_zip_dirs = tflip {
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
} }
local function io_open_absolute(fn, mode) local function io_open_absolute(fn, mode)
-- if file exists, use original mode -- if file exists, use original mode
local res, err = _bllua_io_open(fn, mode) local res, err = _bllua_io_open(fn, mode)
if res then return res end if res then return res end
-- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader -- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader
local dir = fn:match('^[^/]+') 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, 'File is not in one of the allowed directories' end
local exist = _bllua_ts.call('isFile', fn) == '1' local exist = _bllua_ts.call('isFile', fn) == '1'
if not exist then return nil, err end if not exist then return nil, err end
if mode ~= nil and mode ~= 'r' and mode ~= 'rb' then if mode~=nil and mode~='r' and mode~='rb' then
return nil, 'Files in zips can only be opened in read mode' return nil, 'Files in zips can only be opened in read mode' end
end
-- return a temp lua file object with the data
-- return a temp lua file object with the data local fi = new_file_obj(fn)
local fi = new_file_obj(fn) return fi
return fi
end end
io = io or {} io = io or {}
---@diagnostic disable-next-line: duplicate-set-field
function io.open(fn, mode, errn) function io.open(fn, mode, errn)
errn = errn or 1 errn = errn or 1
-- try to open the file with relative path, otherwise use absolute path -- try to open the file with relative path, otherwise use absolute path
local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File') local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File')
if curfn == '' then curfn = nil end if curfn == '' then curfn = nil end
if fn:find('^%.') then if fn:find('^%.') then
local relfn = curfn and fn:find('^%./') and local relfn = curfn and fn:find('^%./') and
curfn:gsub('[^/]+$', '') .. fn:gsub('^%./', '') curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '')
if relfn then if relfn then
local fi, err = io_open_absolute(relfn, mode) local fi, err = io_open_absolute(relfn, mode)
return fi, err, relfn return fi, err, relfn
else else
return nil, 'Invalid path', fn return nil, 'Invalid path', fn
end end
else else
local fi, err = io_open_absolute(fn, mode) local fi, err = io_open_absolute(fn, mode)
return fi, err, fn return fi, err, fn
end end
end end
---@diagnostic disable-next-line: duplicate-set-field
function io.lines(fn) function io.lines(fn)
local fi, err, fn2 = io.open(fn, 'r', 2) local fi, err, fn2 = io.open(fn, nil, 2)
if not fi then error('Error opening file \'' .. tostring(fn2) .. '\': ' .. tostring(err), 2) end if not fi then error('Error opening file \''..fn2..'\': '..err, 2) end
return fi:lines() return fi:lines()
end end
---@diagnostic disable-next-line: duplicate-set-field
function io.type(f) function io.type(f)
---@diagnostic disable-next-line: undefined-field if type(f)=='table' and f._is_file then
if type(f) == 'table' and f._is_file then return f._is_open and 'file' or 'closed file'
---@diagnostic disable-next-line: undefined-field else
return f._is_open and 'file' or 'closed file' return _bllua_io_type(f)
else end
return _bllua_io_type(f)
end
end end
-- provide dofile -- provide dofile
function dofile(fn, errn) function dofile(fn, errn)
errn = errn or 1 errn = errn or 1
local fi, err, fn2 = io.open(fn, 'r', errn + 1) local fi, err, fn2 = io.open(fn, 'r', errn+1)
if not fi then error('Error executing file \'' .. tostring(fn2) .. '\': ' .. tostring(err), errn + 1) end if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end
print('Executing ' .. fn2) print('Executing '..fn2)
local text = fi:read('*a') local text = fi:read('*a')
fi:close() fi:close()
return assert(loadstring('--[[' .. fn2 .. ']]' .. text))() return assert(loadstring('--[['..fn2..']]'..text))()
end end
-- provide require (just a wrapper for dofile) -- provide require (just a wrapper for dofile)
@@ -170,67 +158,57 @@ end
-- blockland directory -- blockland directory
-- current add-on -- current add-on
local function file_exists(fn, errn) local function file_exists(fn, errn)
local fi, err, fn2 = io.open(fn, 'r', errn + 1) local fi, err, fn2 = io.open(fn, 'r', errn+1)
if fi then if fi then
fi:close() fi:close()
return fn2 return fn2
else else
return nil return nil
end end
end end
local require_memo = {} local require_memo = {}
function require(mod) function require(mod)
if require_memo[mod] then return unpack(require_memo[mod]) end if require_memo[mod] then return unpack(require_memo[mod]) end
local fp = mod:gsub('%.', '/') local fp = mod:gsub('%.', '/')
local fns = { local fns = {
'./' .. fp .. '.lua', -- local file './'..fp..'.lua', -- local file
'./' .. fp .. '/init.lua', -- local library './'..fp..'/init.lua', -- local library
fp .. '.lua', -- global file fp..'.lua', -- global file
fp .. '/init.lua', -- global library fp..'/init.lua', -- global library
} }
if fp:lower():find('^add-ons/') then if fp:lower():find('^add-ons/') then
local addonpath = fp:lower():match('^add-ons/[^/]+') .. '/' local addonpath = fp:lower():match('^add-ons/[^/]+')..'/'
table.insert(fns, addonpath .. fp .. '.lua') -- add-on file table.insert(fns, addonpath..fp..'.lua') -- add-on file
table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library table.insert(fns, addonpath..fp..'/init.lua') -- add-on library
end end
for _, fn in ipairs(fns) do for _,fn in ipairs(fns) do
local fne = file_exists(fn, 2) local fne = file_exists(fn, 2)
if fne then if fne then
local res = { dofile(fne, 2) } local res = {dofile(fne, 2)}
require_memo[mod] = res require_memo[mod] = res
return unpack(res) return unpack(res)
end end
end end
return _bllua_requiresecure(mod) return _bllua_requiresecure(mod)
end 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 function isValidCode(code)
local f, e = loadstring(code) local f,e = loadstring(code)
return f ~= nil return f~=nil
end end
function _bllua_smarteval(code) function _bllua_smarteval(code)
if (not code:find('^print%(')) and isValidCode('print(' .. code .. ')') then if (not code:find('^print%(')) and isValidCode('print('..code..')') then
code = 'print(' .. code .. ')' code = 'print('..code..')' end
end local f,e = loadstring(code)
local f, e = loadstring(code) if f then
if f then return f()
return f() else
else print(e)
print(e) end
end
end end
function ts.setvar(name, val) function ts.setvar(name, val)
_bllua_ts.call('_bllua_set_var', name, val) _bllua_ts.call('_bllua_set_var', name, val)
end end
_bllua_ts.call('echo', ' Executed libts-lua.lua') _bllua_ts.call('echo', ' Executed libts-lua.lua')

View File

@@ -49,26 +49,4 @@ function _bllua_set_var(%name, %val) {
return ""; return "";
} }
// 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 libts-ts.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

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

View File

@@ -235,4 +235,7 @@ vector_new = function(vi)
end end
vector = vector_new vector = vector_new
print(' Executed vector.lua')
return vector_new return vector_new