diff --git a/inc/lua/luaconf.h b/inc/lua/luaconf.h index 64a789a..7ce5617 100644 --- a/inc/lua/luaconf.h +++ b/inc/lua/luaconf.h @@ -9,6 +9,7 @@ #ifndef WINVER #define WINVER 0x0501 #endif + #include #include diff --git a/src/luainterp.cpp b/src/luainterp.cpp index a202f6a..8b3aa34 100644 --- a/src/luainterp.cpp +++ b/src/luainterp.cpp @@ -5,6 +5,7 @@ #include "lauxlib.h" #include "lua.h" #include + int bll_error_handler(lua_State *L) { lua_getfield(L, LUA_GLOBALSINDEX, "_bllua_on_error"); if (!lua_isfunction(L, -1)) { diff --git a/src/luainterp.hpp b/src/luainterp.hpp index e672ca5..361c204 100644 --- a/src/luainterp.hpp +++ b/src/luainterp.hpp @@ -3,6 +3,7 @@ #define _H_LUAINTERP_SHARED #include "lua.h" + #define BLL_ARG_COUNT 20 #define BLL_ARG_MAX 8192 diff --git a/src/lualibts.cpp b/src/lualibts.cpp index 7283c70..7f2ce21 100644 --- a/src/lualibts.cpp +++ b/src/lualibts.cpp @@ -8,6 +8,7 @@ #include "lauxlib.h" #include "lua.h" #include "luainterp.hpp" + int bll_TsCall(lua_State *L, const char *oname, const char *fname, int argc, int ofs) { ADDR obj = (ADDR)NULL; diff --git a/src/tsliblua.cpp b/src/tsliblua.cpp index 1448d1f..f2c85f9 100644 --- a/src/tsliblua.cpp +++ b/src/tsliblua.cpp @@ -4,6 +4,7 @@ #include "BlFuncs.hpp" #include "BlHooks.hpp" #include "luainterp.hpp" + bool bll_LuaCall(const char *fname, int argc, const char *argv[]) { lua_getglobal(gL, fname); for (int i = 0; i < argc; i++) { diff --git a/src/util/libbl.lua b/src/util/libbl.lua index 52195df..e3f0f87 100644 --- a/src/util/libbl.lua +++ b/src/util/libbl.lua @@ -202,11 +202,11 @@ setForceType = function(ftname, typ) 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 @@ -296,7 +296,7 @@ function bl.class(cname, inhname) 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 cname = cname:lower() - + local met = bl._objectUserMetas[cname] or { _name = cname, _inherit = nil, @@ -304,22 +304,22 @@ function bl.class(cname, inhname) } bl._objectUserMetas[cname] = met setmetatable(met, tsClassMeta) - + if inhname then inhname = inhname:lower() - + 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 - + 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 - + -- apply inherited method and field types for ftname, typ in pairs(bl._forceType) do local cname2, rest = classFromForceTypeStr(ftname) @@ -521,11 +521,11 @@ toTsObject = function(idiS) error('toTsObject: input must be a string', 2) end idiS = idiS:lower() if bl._objectsWeak[idiS] then return bl._objectsWeak[idiS] end - + if not tsIsObject(idiS) then --error('toTsObject: object \''..idiS..'\' does not exist', 2) end return nil end - + local className = _bllua_ts.callobj(idiS, 'getClassName') local obj = { _tsObjectId = _bllua_ts.callobj(idiS, 'getId' ), @@ -534,7 +534,7 @@ toTsObject = function(idiS) _tsClassName = className:lower(), } setmetatable(obj, tsObjectMeta) - + bl._objectsWeak[obj._tsObjectId ] = obj bl._objectsWeak[obj._tsName:lower()] = obj return obj @@ -856,11 +856,11 @@ function bl.hook(pkg, name, time, func) 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 - + bl._hooks[pkg] = bl._hooks[pkg] or {} bl._hooks[pkg][name] = bl._hooks[pkg][name] or {} bl._hooks[pkg][name][time] = func - + updateHook(pkg, name, bl._hooks[pkg][name]) activatePackage(pkg) end @@ -874,7 +874,7 @@ function bl.unhook(pkg, name, time) error('bl.unhook: argument #2: invalid function name \''..tostring(name)..'\'', 2) end if 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 for name,hk in pairs(bl._hooks[pkg]) do @@ -961,9 +961,9 @@ function bl.raycast(start, stop, mask, ignores) for _,v in ipairs(ignores) do table.insert(ignoresS, objToTs(v)) end - + local retS = _bllua_ts.call('containerRaycast', startS, stopS, maskS, unpack(ignoresS)) - + if retS=='0' then return nil else @@ -988,7 +988,7 @@ function bl.boxSearch(pos, size, mask) local posS = vecToTs(pos) local sizeS = vecToTs(size) local maskS = maskToTs(mask) - + _bllua_ts.call('initContainerBoxSearch', posS, sizeS, maskS) return tsContainerSearchIterator end @@ -998,7 +998,7 @@ function bl.radiusSearch(pos, radius, mask) error('bl.radiusSearch: argument #2: radius must be a number', 2) end local radiusS = tostring(radius) local maskS = maskToTs(mask) - + _bllua_ts.call('initContainerRadiusSearch', posS, radiusS, maskS) return tsContainerSearchIterator end @@ -1037,7 +1037,7 @@ local function createTsObj(keyword, class, name, inherit, props) table.insert(propsT, k..'="'..valToTs(v)..'";') end end - + local objS = _bllua_ts.eval( 'return '..keyword..' '..class..'('.. (name or '')..(inherit and (':'..inherit) or '')..'){'.. @@ -1045,7 +1045,7 @@ local function createTsObj(keyword, class, name, inherit, props) local obj = toTsObject(objS) if not obj then error('bl.new/bl.datablock: failed to create object', 3) end - + return obj end local function parseTsDecl(decl) diff --git a/src/util/libts-lua.lua b/src/util/libts-lua.lua index 56f4a9d..ebf3d29 100644 --- a/src/util/libts-lua.lua +++ b/src/util/libts-lua.lua @@ -1,4 +1,3 @@ - -- This Lua code provides some built-in utilities for writing Lua add-ons -- It is eval'd automatically once BLLua3 has loaded the TS API and environment -- It only has access to the sandboxed lua environment, just like user code. @@ -7,8 +6,11 @@ ts = _bllua_ts -- Provide limited OS functions os = os or {} -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.time() return math.floor(tonumber(_bllua_ts.call('getSimTime')) / 1000) end + +---@diagnostic disable-next-line: duplicate-set-field +function os.clock() return tonumber(_bllua_ts.call('getSimTime')) / 1000 end -- Virtual file class, emulating a file object as returned by io.open -- Used to wrap io.open to allow reading from zips (using TS) @@ -16,140 +18,150 @@ function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end -- Can't read nulls, can't distinguish between CRLF and LF. -- Todo someday: actually read the zip in lua? local file_meta = { - read = function(file, mode) - file:_init() - if not file or type(file)~='table' or not file._is_file then error('File:read: Not a file', 2) end - if file._is_open ~= true then error('File:read: File is closed', 2) end - if mode=='*n' then - local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) - if n then - file.pos = file.pos + #ws + #n - return n - else - return nil - end - elseif mode=='*a' then - local d = file.data:sub(file.pos, #file.data) - file.pos = #file.data + 1 - return d - elseif mode=='*l' then - local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) - if not l then - l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; - if l=='' then return nil end - end - if l then - file.pos = file.pos + #l + #ws - return l - else - return nil - end - elseif type(mode)=='number' then - local d = file.data:sub(file.pos, file.pos+mode) - file.pos = file.pos + #d - return d - else - error('File:read: Invalid mode \''..mode..'\'', 2) - end - end, - lines = function(file) - file:_init() - return function() - return file:read('*l') - end - end, - close = function(file) - if not file._is_open then error('File:close: File is not open', 2) end - file._is_open = false - end, - __index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end, - _init = function(f) - if not f.data then - f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename) - end - end, + read = function(file, mode) + file:_init() + if not file or type(file) ~= 'table' or not file._is_file then error('File:read: Not a file', 2) end + if file._is_open ~= true then error('File:read: File is closed', 2) end + if mode == '*n' then + local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos) + if n then + file.pos = file.pos + #ws + #n + return n + else + return nil + end + elseif mode == '*a' then + local d = file.data:sub(file.pos, #file.data) + file.pos = #file.data + 1 + return d + elseif mode == '*l' then + local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos) + if not l then + l = file.data:match('^([^\r\n]*)$', file.pos); ws = ''; + if l == '' then return nil end + end + if l then + file.pos = file.pos + #l + #ws + return l + else + return nil + end + elseif type(mode) == 'number' then + local d = file.data:sub(file.pos, file.pos + mode) + file.pos = file.pos + #d + return d + else + error('File:read: Invalid mode \'' .. mode .. '\'', 2) + end + end, + lines = function(file) + file:_init() + return function() + return file:read('*l') + end + end, + close = function(file) + if not file._is_open then error('File:close: File is not open', 2) end + file._is_open = false + end, + __index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end, + _init = function(f) + if not f.data then + f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename) + end + end, } local function new_file_obj(fn) - local file = { - _is_file = true, - _is_open = true, - pos = 1, - __index = file_meta.__index, - filename = fn, - data = nil, - } - setmetatable(file, file_meta) - return file + local file = { + _is_file = true, + _is_open = true, + pos = 1, + __index = file_meta.__index, + filename = fn, + data = nil, + } + setmetatable(file, file_meta) + return file end -local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end -local allowed_zip_dirs = tflip{ - 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' +local function tflip(t) + local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; +end +local allowed_zip_dirs = tflip { + 'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders' } local function io_open_absolute(fn, mode) - -- if file exists, use original mode - local res, err = _bllua_io_open(fn, mode) - 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 - local dir = fn:match('^[^/]+') - 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' - if not exist then return nil, err end - - if mode~=nil and mode~='r' and mode~='rb' then - return nil, 'Files in zips can only be opened in read mode' end - - -- return a temp lua file object with the data - local fi = new_file_obj(fn) - return fi + -- if file exists, use original mode + local res, err = _bllua_io_open(fn, mode) + 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 + local dir = fn:match('^[^/]+') + 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' + if not exist then return nil, err end + + if mode ~= nil and mode ~= 'r' and mode ~= 'rb' then + return nil, 'Files in zips can only be opened in read mode' + end + + -- return a temp lua file object with the data + local fi = new_file_obj(fn) + return fi end io = io or {} +---@diagnostic disable-next-line: duplicate-set-field function io.open(fn, mode, errn) - errn = errn or 1 - - -- try to open the file with relative path, otherwise use absolute path - local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File') - if curfn == '' then curfn = nil end - if fn:find('^%.') then - local relfn = curfn and fn:find('^%./') and - curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '') - if relfn then - 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) - return fi, err, fn - end + errn = errn or 1 + + -- try to open the file with relative path, otherwise use absolute path + local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File') + if curfn == '' then curfn = nil end + if fn:find('^%.') then + local relfn = curfn and fn:find('^%./') and + curfn:gsub('[^/]+$', '') .. fn:gsub('^%./', '') + if relfn then + 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) + return fi, err, fn + end end + +---@diagnostic disable-next-line: duplicate-set-field function io.lines(fn) - local fi, err, fn2 = io.open(fn, nil, 2) - if not fi then error('Error opening file \''..fn2..'\': '..err, 2) end - return fi:lines() + local fi, err, fn2 = io.open(fn, nil, 2) + if not fi then error('Error opening file \'' .. fn2 .. '\': ' .. err, 2) end + return fi:lines() end + +---@diagnostic disable-next-line: duplicate-set-field function io.type(f) - if type(f)=='table' and f._is_file then - return f._is_open and 'file' or 'closed file' - else - return _bllua_io_type(f) - end +---@diagnostic disable-next-line: undefined-field + if type(f) == 'table' and f._is_file then +---@diagnostic disable-next-line: undefined-field + return f._is_open and 'file' or 'closed file' + else + return _bllua_io_type(f) + end end -- provide dofile function dofile(fn, errn) - errn = errn or 1 - - local fi, err, fn2 = io.open(fn, 'r', errn+1) - if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end - - print('Executing '..fn2) - local text = fi:read('*a') - fi:close() - return assert(loadstring('--[['..fn2..']]'..text))() + errn = errn or 1 + + local fi, err, fn2 = io.open(fn, 'r', errn + 1) + if not fi then error('Error executing file \'' .. fn2 .. '\': ' .. err, errn + 1) end + + print('Executing ' .. fn2) + local text = fi:read('*a') + fi:close() + return assert(loadstring('--[[' .. fn2 .. ']]' .. text))() end -- provide require (just a wrapper for dofile) @@ -158,57 +170,58 @@ end -- blockland directory -- current add-on local function file_exists(fn, errn) - local fi, err, fn2 = io.open(fn, 'r', errn+1) - if fi then - fi:close() - return fn2 - else - return nil - end + local fi, err, fn2 = io.open(fn, 'r', errn + 1) + if fi then + fi:close() + return fn2 + else + return nil + end end local require_memo = {} function require(mod) - if require_memo[mod] then return unpack(require_memo[mod]) end - local fp = mod:gsub('%.', '/') - local fns = { - './'..fp..'.lua', -- local file - './'..fp..'/init.lua', -- local library - fp..'.lua', -- global file - fp..'/init.lua', -- global library - } - if fp:lower():find('^add-ons/') then - local addonpath = fp:lower():match('^add-ons/[^/]+')..'/' - table.insert(fns, addonpath..fp..'.lua') -- add-on file - table.insert(fns, addonpath..fp..'/init.lua') -- add-on library - end - for _,fn in ipairs(fns) do - local fne = file_exists(fn, 2) - if fne then - local res = {dofile(fne, 2)} - require_memo[mod] = res - return unpack(res) - end - end - return _bllua_requiresecure(mod) + if require_memo[mod] then return unpack(require_memo[mod]) end + local fp = mod:gsub('%.', '/') + local fns = { + './' .. fp .. '.lua', -- local file + './' .. fp .. '/init.lua', -- local library + fp .. '.lua', -- global file + fp .. '/init.lua', -- global library + } + if fp:lower():find('^add-ons/') then + local addonpath = fp:lower():match('^add-ons/[^/]+') .. '/' + table.insert(fns, addonpath .. fp .. '.lua') -- add-on file + table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library + end + for _, fn in ipairs(fns) do + local fne = file_exists(fn, 2) + if fne then + local res = { dofile(fne, 2) } + require_memo[mod] = res + return unpack(res) + end + end + return _bllua_requiresecure(mod) end local function isValidCode(code) - local f,e = loadstring(code) - return f~=nil + local f, e = loadstring(code) + return f ~= nil end function _bllua_smarteval(code) - if (not code:find('^print%(')) and isValidCode('print('..code..')') then - code = 'print('..code..')' end - local f,e = loadstring(code) - if f then - return f() - else - print(e) - end + if (not code:find('^print%(')) and isValidCode('print(' .. code .. ')') then + code = 'print(' .. code .. ')' + end + local f, e = loadstring(code) + if f then + return f() + else + print(e) + end end function ts.setvar(name, val) - _bllua_ts.call('_bllua_set_var', name, val) + _bllua_ts.call('_bllua_set_var', name, val) end _bllua_ts.call('echo', ' Executed libts-lua.lua') diff --git a/src/util/matrix.lua b/src/util/matrix.lua index 41a3542..8c16791 100644 --- a/src/util/matrix.lua +++ b/src/util/matrix.lua @@ -1,4 +1,3 @@ - -- todo -- Matrix class with math operators diff --git a/src/util/std.lua b/src/util/std.lua index ca79eea..ce57486 100644 --- a/src/util/std.lua +++ b/src/util/std.lua @@ -1,350 +1,376 @@ - -- Basic functionality that should be standard in Lua -- Table / List -- Whether a table contains no keys function table.empty(t) - return next(t)~=nil + return next(t) ~= nil end + -- Apply a function to each key in a table function table.map(f, ...) - local ts = {...} - local 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)) - end - return u + local ts = { ... } + 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)) + end + return u end + function table.map_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(unpack(args)) - end - return u + 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(unpack(args)) + end + return u end + -- Swap keys/values function table.swap(t) - local u = {} - for k,v in pairs(t) do u[v] = k end - return u + local u = {} + for k, v in pairs(t) do u[v] = k end + return u end + -- Reverse a list function table.reverse(l) - local m = {} - for i=1,#l do m[#l-i+1] = l[i] end - return m + local m = {} + for i = 1, #l do m[#l - i + 1] = l[i] end + return m end + -- Whether a table is a list/array (has only monotonic integer keys) function table.islist(t) - local n = 0 - for i,_ in pairs(t) do - if type(i)~='number' or i%1~=0 then return false end - n = n+1 - end - return n==#t + local n = 0 + for i, _ in pairs(t) do + if type(i) ~= 'number' or i % 1 ~= 0 then return false end + n = n + 1 + end + return n == #t end + -- Append contents of other tables to first table function table.append(t, ...) - local a = {...} - for _,u in ipairs(a) do - for _,v in ipairs(u) do table.insert(t,v) end - end - return t + local a = { ... } + for _, u in ipairs(a) do + for _, v in ipairs(u) do table.insert(t, v) end + end + return t end + -- Create a new table containing all keys from any number of tables -- latter tables in the arg list override prior ones -- overlaps, NOT appends, integer keys function table.join(...) - local ts = {...} - local w = {} - for _,t in ipairs(ts) do - for k,v in pairs(t) do w[k] = v end - end - return w + local ts = { ... } + local w = {} + for _, t in ipairs(ts) do + for k, v in pairs(t) do w[k] = v end + end + return w end + -- Whether a table contains a certain value in any key -function table.contains(t,s) - for _,v in pairs(t) do - if v==s then return true end - end - return false +function table.contains(t, s) + for _, v in pairs(t) do + if v == s then return true end + end + return false end -function table.contains_list(t,s) - for _,v in ipairs(t) do - if v==s then return true end - end - return false + +function table.contains_list(t, s) + for _, v in ipairs(t) do + if v == s then return true end + end + return false end + -- Copy a table to another table function table.copy(t) - local u = {} - for k,v in pairs(t) do u[k] = v end - return u + local u = {} + for k, v in pairs(t) do u[k] = v end + return u end + function table.copy_list(l) - local m = {} - for i,v in ipairs(l) do m[i] = v end - return m + local m = {} + for i, v in ipairs(l) do m[i] = v end + return m end + -- Sort a table in a new copy function table.sortcopy(t, f) - local u = table.copy_list(t) - table.sort(u, f) - return u + local u = table.copy_list(t) + table.sort(u, f) + return u end + -- Remove a value from a table function table.removevalue(t, r) - local rem = {} - for k,v in pairs(t) do - if v==r then table.insert(rem, k) end - end - for _,k in ipairs(rem) do t[k] = nil end + local rem = {} + for k, v in pairs(t) do + if v == r then table.insert(rem, k) end + end + for _, k in ipairs(rem) do t[k] = nil end end + function table.removevalue_list(t, r) - for i = #t, 1, -1 do - if t[i]==r then - table.remove(t, i) - end - end + for i = #t, 1, -1 do + if t[i] == r then + table.remove(t, i) + end + end end + -- Export tables into formatted executable strings local function tabs(tabLevel) - return (' '):rep(tabLevel) + return (' '):rep(tabLevel) end local valueToString local function tableToString(t, tabLevel, seen) - if type(t)~='table' or (getmetatable(t) and getmetatable(t).__tostring) then - return tostring(t) - elseif table.islist(t) then - if #t==0 then - return '{}' - else - local strs = {} - local containsTables = false - for _,v in ipairs(t) do - if type(v)=='table' then containsTables = true end - table.insert(strs, valueToString(v, tabLevel+1, seen)..',') - end - if containsTables or #t>3 then - return '{\n'..tabs(tabLevel+1) - ..table.concat(strs, '\n'..tabs(tabLevel+1)) - ..'\n'..tabs(tabLevel)..'}' - else - return '{ '..table.concat(strs, ' ')..' }' - end - end - else - local containsNonStringKeys = false - for k,v in pairs(t) do - if type(k)~='string' or k:find('[^a-zA-Z0-9_]') then - containsNonStringKeys = true - elseif type(k)=='table' then - error('table.tostring: table contains a table as key, cannot serialize') - end - end - local strs = {} - if containsNonStringKeys then - for k,v in pairs(t) do - table.insert(strs, '\n'..tabs(tabLevel+1) - ..'['..valueToString(k, tabLevel+1, seen)..'] = ' - ..valueToString(v, tabLevel+1, seen)..',') - end - else - for k,v in pairs(t) do - table.insert(strs, '\n'..tabs(tabLevel+1) - ..k..' = '..valueToString(v, tabLevel+1, seen)..',') - end - end - return '{'..table.concat(strs)..'\n'..tabs(tabLevel)..'}' - end + if type(t) ~= 'table' or (getmetatable(t) and getmetatable(t).__tostring) then + return tostring(t) + elseif table.islist(t) then + if #t == 0 then + return '{}' + else + local strs = {} + local containsTables = false + for _, v in ipairs(t) do + if type(v) == 'table' then containsTables = true end + table.insert(strs, valueToString(v, tabLevel + 1, seen) .. ',') + end + if containsTables or #t > 3 then + return '{\n' .. tabs(tabLevel + 1) + .. table.concat(strs, '\n' .. tabs(tabLevel + 1)) + .. '\n' .. tabs(tabLevel) .. '}' + else + return '{ ' .. table.concat(strs, ' ') .. ' }' + end + end + else + local containsNonStringKeys = false + for k, v in pairs(t) do + if type(k) ~= 'string' or k:find('[^a-zA-Z0-9_]') then + containsNonStringKeys = true + elseif type(k) == 'table' then + error('table.tostring: table contains a table as key, cannot serialize') + end + end + local strs = {} + if containsNonStringKeys then + for k, v in pairs(t) do + table.insert(strs, '\n' .. tabs(tabLevel + 1) + .. '[' .. valueToString(k, tabLevel + 1, seen) .. '] = ' + .. valueToString(v, tabLevel + 1, seen) .. ',') + end + else + for k, v in pairs(t) do + table.insert(strs, '\n' .. tabs(tabLevel + 1) + .. k .. ' = ' .. valueToString(v, tabLevel + 1, seen) .. ',') + end + end + return '{' .. table.concat(strs) .. '\n' .. tabs(tabLevel) .. '}' + end end valueToString = function(v, tabLevel, seen) - local t = type(v) - if t=='table' then - if seen[v] then - return 'nil --[[ already seen: '..tostring(v)..' ]]' - else - seen[v] = true - return tableToString(v, tabLevel, seen) - end - elseif t=='string' then - return '\''..string.escape(v)..'\'' - elseif t=='number' or t=='boolean' then - return tostring(v) - else - --error('table.tostring: table contains a '..t..' value, cannot serialize') - return 'nil --[[ cannot serialize '..tostring(v)..' ]]' - end + local t = type(v) + if t == 'table' then + if seen[v] then + return 'nil --[[ already seen: ' .. tostring(v) .. ' ]]' + else + seen[v] = true + return tableToString(v, tabLevel, seen) + end + elseif t == 'string' then + return '\'' .. string.escape(v) .. '\'' + elseif t == 'number' or t == 'boolean' then + return tostring(v) + else + --error('table.tostring: table contains a '..t..' value, cannot serialize') + return 'nil --[[ cannot serialize ' .. tostring(v) .. ' ]]' + end end function table.tostring(t) - return tableToString(t, 0, {}) + return tableToString(t, 0, {}) end - -- String -- Split string into table by separator -- or by chars if no separator given -- if regex is not true, sep is treated as a regex pattern function string.split(str, sep, noregex) - if type(str)~='string' then - error('string.split: argument #1: expected string, got '..type(str), 2) end - if sep==nil or sep=='' then - local t = {} - local ns = #str - for x = 1, ns do - table.insert(t, str:sub(x, x)) - end - return t - elseif type(sep)=='string' then - local t = {} - if #str>0 then - local first = 1 - while true do - local last, newfirst = str:find(sep, first, noregex) - if not last then break end - table.insert(t, str:sub(first, last-1)) - first = newfirst+1 - end - table.insert(t, str:sub(first, #str)) - end - return t - else - error( - 'string.split: argument #2: expected string or nil, got '..type(sep), 2) - end -end --- Split string to a list of char bytes -function string.bytes(s) - local b = {} - for i=1,#s do - local c = s:sub(i,i) - table.insert(b, c:byte()) - end - return b -end --- Trim leading and trailing whitespace -function string.trim(s, ws) - ws = ws or ' \t\r\n' - return s:gsub('^['..ws..']+', ''):gsub('['..ws..']+$', '')..'' -end --- String slicing and searching using [] operator -local str_meta = getmetatable('') -local str_meta_index_old= str_meta.__index -function str_meta.__index(s,k) - if type(k)=='string' then - return str_meta_index_old[k] - elseif type(k)=='number' then - if k<0 then k = #s+k+1 end - return string.sub(s,k,k) - elseif type(k)=='table' then - local a = k[1]<0 and (#s+k[1]+1) or k[1] - local b = k[2]<0 and (#s+k[2]+1) or k[2] - return string.sub(s,a,b) - end -end --- String iterator -function string.chars(s) - local i = 0 - return function() - i = i+1 - if i<=#s then return s:sub(i,i) - else return nil end - end -end --- Escape sequences -local defaultEscapes = { - ['\\'] = '\\\\', - ['\''] = '\\\'', - ['\"'] = '\\\"', - ['\t'] = '\\t', - ['\r'] = '\\r', - ['\n'] = '\\n', - ['\0'] = '\\0', -} -function string.escape(s, escapes) - escapes = escapes or defaultEscapes - local t = {} - for i=1,#s do - local c = s:sub(i,i) - table.insert(t, escapes[c] or c) - end - return table.concat(t) -end -local defaultEscapeChar = '\\' -local defaultUnescapes = { - ['\\'] = '\\', - ['\''] = '\'', - ['\"'] = '\"', - ['t'] = '\t', - ['r'] = '\r', - ['n'] = '\n', - ['0'] = '\0', -} -function string.unescape(s, escapeChar, unescapes) - escapeChar = escapeChar or defaultEscapeChar - unescapes = unescapes or defaultUnescapes - local t = {} - local inEscape = false - for i=1,#s do - local c = s:sub(i,i) - if inEscape then - table.insert(t, unescapes[c] - or error('string.unescape: invalid escape sequence: \'' - ..escapeChar..c..'\'')) - elseif c==escapeChar then - inEscape = true - else - table.insert(t, c) - end - end - return table.concat(t) + if type(str) ~= 'string' then + error('string.split: argument #1: expected string, got ' .. type(str), 2) + end + if sep == nil or sep == '' then + local t = {} + local ns = #str + for x = 1, ns do + table.insert(t, str:sub(x, x)) + end + return t + elseif type(sep) == 'string' then + local t = {} + if #str > 0 then + local first = 1 + while true do + local last, newfirst = str:find(sep, first, noregex) + if not last then break end + table.insert(t, str:sub(first, last - 1)) + first = newfirst + 1 + end + table.insert(t, str:sub(first, #str)) + end + return t + else + error( + 'string.split: argument #2: expected string or nil, got ' .. type(sep), 2) + end end +-- Split string to a list of char bytes +function string.bytes(s) + local b = {} + for i = 1, #s do + local c = s:sub(i, i) + table.insert(b, c:byte()) + end + return b +end + +-- Trim leading and trailing whitespace +function string.trim(s, ws) + ws = ws or ' \t\r\n' + return s:gsub('^[' .. ws .. ']+', ''):gsub('[' .. ws .. ']+$', '') .. '' +end + +-- String slicing and searching using [] operator +local str_meta = getmetatable('') +local str_meta_index_old = str_meta.__index +function str_meta.__index(s, k) + if type(k) == 'string' then + return str_meta_index_old[k] + elseif type(k) == 'number' then + if k < 0 then k = #s + k + 1 end + return string.sub(s, k, k) + elseif type(k) == 'table' then + local a = k[1] < 0 and (#s + k[1] + 1) or k[1] + local b = k[2] < 0 and (#s + k[2] + 1) or k[2] + return string.sub(s, a, b) + end +end + +-- String iterator +function string.chars(s) + local i = 0 + return function() + i = i + 1 + if i <= #s then + return s:sub(i, i) + else + return nil + end + end +end + +-- Escape sequences +local defaultEscapes = { + ['\\'] = '\\\\', + ['\''] = '\\\'', + ['\"'] = '\\\"', + ['\t'] = '\\t', + ['\r'] = '\\r', + ['\n'] = '\\n', + ['\0'] = '\\0', +} +function string.escape(s, escapes) + escapes = escapes or defaultEscapes + local t = {} + for i = 1, #s do + local c = s:sub(i, i) + table.insert(t, escapes[c] or c) + end + return table.concat(t) +end + +local defaultEscapeChar = '\\' +local defaultUnescapes = { + ['\\'] = '\\', + ['\''] = '\'', + ['\"'] = '\"', + ['t'] = '\t', + ['r'] = '\r', + ['n'] = '\n', + ['0'] = '\0', +} +function string.unescape(s, escapeChar, unescapes) + escapeChar = escapeChar or defaultEscapeChar + unescapes = unescapes or defaultUnescapes + local t = {} + local inEscape = false + for i = 1, #s do + local c = s:sub(i, i) + if inEscape then + table.insert(t, unescapes[c] + or error('string.unescape: invalid escape sequence: \'' + .. escapeChar .. c .. '\'')) + elseif c == escapeChar then + inEscape = true + else + table.insert(t, c) + end + end + return table.concat(t) +end -- IO io = io or {} -- Read entire file at once, return nil,err if access failed function io.readall(filename) - local fi,err = io.open(filename, 'rb') - if not fi then return nil,err end - local s = fi:read("*a") - fi:close() - return s -end --- Write data to file all at once, return true if success / false,err if failure -function io.writeall(filename, data) - local fi,err = io.open(filename, 'wb') - if not fi then return false,err end - fi:write(data) - fi:close() - return true,nil + local fi, err = io.open(filename, 'rb') + if not fi then return nil, err end + local s = fi:read("*a") + fi:close() + return s end +-- Write data to file all at once, return true if success / false,err if failure +function io.writeall(filename, data) + local fi, err = io.open(filename, 'wb') + if not fi then return false, err end + fi:write(data) + fi:close() + return true, nil +end -- Math -- Round function math.round(x) - return math.floor(x+0.5) + return math.floor(x + 0.5) end + -- Mod that accounts for floating point inaccuracy -function math.mod(a,b) - local m = a%b - if m==0 or math.abs(m)<1e-15 or math.abs(m-b)<1e-15 then return 0 - else return m end +function math.mod(a, b) + local m = a % b + if m == 0 or math.abs(m) < 1e-15 or math.abs(m - b) < 1e-15 then + return 0 + else + return m + end end + -- Clamp value between min and max function math.clamp(v, n, x) - return math.min(x, math.max(v, n)) + return math.min(x, math.max(v, n)) end - print(' Executed std.lua')