-- 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. 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 -- Virtual file class, emulating a file object as returned by io.open -- Used to wrap io.open to allow reading from zips (using TS) -- Not perfect because TS file I/O sucks -- 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, } 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 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 io_open_absolute(fn, mode) -- if file exists, use original mode local res, err = _bllua_io_open(fn, mode) 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, '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 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 {} 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 end 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() end 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 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))() end -- provide require (just a wrapper for dofile) -- searches for ?.lua and ?/init.lua in the following directories: -- location of current file -- 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 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) end local function isValidCode(code) 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 end function ts.setvar(name, val) _bllua_ts.call('_bllua_set_var', name, val) end _bllua_ts.call('echo', ' Executed libts-lua.lua')