diff --git a/BlockLua.dll b/BlockLua.dll index a11bb18..e50fcb0 100644 Binary files a/BlockLua.dll and b/BlockLua.dll differ diff --git a/readme.md b/readme.md index e78f471..525343a 100644 --- a/readme.md +++ b/readme.md @@ -14,10 +14,10 @@ Lua scripting for Blockland ### From TorqueScript `'print('hello world')` - Execute Lua in the console by prepending a `'` (single quote) `luaeval("code");` - Execute 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", %value);` - Write a Lua global variable +`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 @@ -35,7 +35,7 @@ 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 @@ -48,15 +48,25 @@ Lua scripting for Blockland `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. +### List of Object Classes (for raycasts and searches) +`'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'` + ### Server-Client Communication -`bl.addServerCmd('commandName', function(client, args...) yourCode end)` - Register a /command on the server -`bl.addClientCmd('commandName', function(args...) yourCode 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.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...)` - As a client, execute a server command +`bl.commandToClient(client, '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', function(args) yourCode end)` - Hook a Torque function with a Lua function. +`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. @@ -136,14 +146,16 @@ When reading from outside ZIPs, binary files are fully supported. WIP ### Extended Standard Lua Library -`string[index]` -`string[{start,stop}]` +`str[index]` +`str[{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)` @@ -173,43 +185,35 @@ TorqueScript stores no type information; all values in TorqueScript are strings. ### 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 -- 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` +- 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" in TorqueScript parlance), and is converted into a `matrix` representing the translation and rotation. +- (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 objects by hand, use `bl.object`, `bl.boolean`, or `bl.string`. +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 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 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. -Please do not publish add-ons that require unsafe mode. - -### List of Object Types -`'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'` +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. ## 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 src/bllua` - +`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/ diff --git a/src/bllua4.cpp b/src/bllua4.cpp index 7286212..a07772b 100644 --- a/src/bllua4.cpp +++ b/src/bllua4.cpp @@ -1,4 +1,4 @@ -// BlockLua (bllua4): Simple Lua interface for TorqueScript +// BlockLua (bllua4): Advanced Lua interface for TorqueScript // Includes @@ -60,23 +60,27 @@ 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_fileLuaMatrix); - BLL_LOAD_LUA(gL, bll_fileLuaLibts); BlEval(bll_fileTsLibts); - BLL_LOAD_LUA(gL, bll_fileLuaLibbl); - BLL_LOAD_LUA(gL, bll_fileLuaLibblTypes); BlEval(bll_fileTsLibblSupport); BlEval(bll_fileLoadaddons); @@ -89,8 +93,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); diff --git a/src/lua-env-safe.lua b/src/lua-env-safe.lua index 07f485a..2249c07 100644 --- a/src/lua-env-safe.lua +++ b/src/lua-env-safe.lua @@ -14,6 +14,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 { @@ -39,13 +40,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, @@ -81,14 +79,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 @@ -120,6 +119,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 diff --git a/src/lua-env.lua b/src/lua-env.lua index 761a23d..573088d 100644 --- a/src/lua-env.lua +++ b/src/lua-env.lua @@ -37,5 +37,9 @@ function _bllua_on_error(err) return table.concat(tracelines, '\n') end +-- 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') diff --git a/src/util/libbl.lua b/src/util/libbl.lua index e3f0f87..8abe920 100644 --- a/src/util/libbl.lua +++ b/src/util/libbl.lua @@ -6,15 +6,30 @@ 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-Z_][a-zA-Z0-9_]*$') end @@ -107,13 +122,14 @@ end -- Type conversion from TS to Lua local fromTsForceTypes = { - ['boolean'] = tsBool, - ['object'] = function(val) toTsObject(val) end, -- wrap because toTsObject not defined yet + ['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) - return fromTsForceTypes[typ](val) or - error('valFromTs: invalid force type '..typ, 4) + 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]+)$') @@ -154,7 +170,8 @@ local function multinumericFromTs(val) end end bl._forceType = bl._forceType or {} -local function valFromTs(val, name, name2) -- todo: ensure name and name2 are already lowercase +-- 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 @@ -177,18 +194,26 @@ local function valFromTs(val, name, name2) -- todo: ensure name and name2 are al -- vector, box, or axis->matrix local vec = multinumericFromTs(val) if vec then return vec end + -- net string + if val:sub(1,1)=='\x01' then + return _bllua_ts.call('getTaggedString', val) + 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 local function classFromForceTypeStr(name) local class, rest = name:match('^([a-zA-Z0-9_]+)(::.+)$') @@ -362,17 +387,19 @@ local tsObjectMeta = { tsIsFunctionNs(rawget(t,'_tsNamespace'), name) or tsIsFunctionNs(rawget(t,'_tsName'), name) then - return function(t, ...) - local args = {...} - local argsS = arglistToTs(args) - return valFromTs( - _bllua_ts.callobj(rawget(t,'_tsObjectId'), name, unpack(argsS)), - rawget(t,'_tsName') and rawget(t,'_tsName')..'::'..name, - rawget(t,'_tsNamespace')..'::'..name) + 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 @@ -437,7 +464,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 @@ -544,14 +572,13 @@ end local function safeNamespaceName(name) return tostring(name:gsub(':', '_')) end -local nscallArgStr = '%a,%b,%c,%d,%e,%f,%g,%h' bl._cachedNamespaceCalls = {} local function tsNamespacedCallTfname(name) local tfname = bl._cachedNamespaceCalls[name] if not tfname then tfname = '_bllua_nscall_'..safeNamespaceName(name) - local tfcode = 'function '..tfname..'('..nscallArgStr..'){'.. - name..'('..nscallArgStr..');}' + local tfcode = 'function '..tfname..'('..tsArgsLocal..'){'.. + name..'('..tsArgsLocal..');}' _bllua_ts.eval(tfcode) bl._cachedNamespaceCalls[name] = tfname end @@ -559,9 +586,9 @@ local function tsNamespacedCallTfname(name) end local function tsCallGen(name) return function(...) - local args = {...} - local argsS = arglistToTs(args) - return valFromTs(_bllua_ts.call(name, unpack(argsS)), name) + local argsS = arglistToTs({...}) + local res = _bllua_ts.call(name, unpack(argsS)) + return valFromTs(res, name) end end @@ -587,14 +614,16 @@ local tsMeta = { 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 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, @@ -613,10 +642,12 @@ function bl.call(func, ...) return _bllua_ts.call(func, unpack(argsS)) end function bl.eval(code) - return valFromTs(_bllua_ts.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.array(name, ...) local rest = {...} @@ -645,12 +676,11 @@ end -- Lua calling from TS local luaLookup luaLookup = function(tbl, name, set, val) - print('lookup', 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 tbl[first]==nil then if set then tbl[first] = {} else return nil end end @@ -661,9 +691,11 @@ luaLookup = function(tbl, name, set, val) 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 + if tbl[first][rest]==nil then error('luacall: no method named \''..rest..'\'', 3) end return function(...) tbl[first][rest](tbl[first], ...) @@ -681,7 +713,7 @@ 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 \''..name..'\'', 2) end + error('luacall: no global in lua named \''..fname..'\'', 2) end local res = func(unpack(args)) return valToTs(res) end @@ -743,9 +775,8 @@ local function addCmd(cmd, func) if not isValidFuncName(cmd) then error('addCmd: invalid function name \''..tostring(cmd)..'\'') end bl._cmds[cmd] = func - local arglist = '%a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p' - _bllua_ts.eval('function '..cmd..'('..arglist..'){'.. - '_bllua_luacall(_bllua_process_cmd,"'..cmd..'",'..arglist..');}') + _bllua_ts.eval('function '..cmd..'('..tsArgsLocal..'){'.. + '_bllua_luacall(_bllua_process_cmd,"'..cmd..'",'..tsArgsLocal..');}') end function bl.addServerCmd(name, func) name = name:lower() @@ -763,8 +794,9 @@ function bl.commandToServer(cmd, ...) _bllua_ts.call('addTaggedString', cmd), unpack(arglistToTs({...}))) end -function bl.commandToClient(cmd, ...) +function bl.commandToClient(client, cmd, ...) _bllua_ts.call('commandToClient', + valToTs(client), _bllua_ts.call('addTaggedString', cmd), unpack(arglistToTs({...}))) end @@ -793,9 +825,6 @@ local function deactivatePackage(pkg) _bllua_ts.call('deactivatePackage', pkg) end end -local hookNargs = 8 -local hookArglistLocal = '%a,%b,%c,%d,%e,%f,%g,%h' -local hookArglistGlobal = '$_bllua_hook_arg1,$_bllua_hook_arg2,$_bllua_hook_arg3,$_bllua_hook_arg4,$_bllua_hook_arg5,$_bllua_hook_arg6,$_bllua_hook_arg7,$_bllua_hook_arg8' bl._hooks = bl._hooks or {} function _bllua_process_hook_before(pkgS, nameS, ...) local args = arglistFromTs(nameS, {...}) @@ -809,7 +838,7 @@ function _bllua_process_hook_before(pkgS, nameS, ...) _bllua_ts.setvar('_bllua_hook_abort', '1') _bllua_ts.setvar('_bllua_hook_return', valToTs(args._return)) end - for i=1,hookNargs do + for i=1,tsMaxArgs do _bllua_ts.setvar('_bllua_hook_arg'..i, valToTs(args[i])) end end @@ -826,13 +855,13 @@ function _bllua_process_hook_after(pkgS, nameS, resultS, ...) end local function updateHook(pkg, name, hk) local beforeCode = hk.before and - ('_bllua_luacall("_bllua_process_hook_before", "'..pkg..'","'..name.. - '",'..hookArglistLocal..');') or '' - local arglist = (hk.before and hookArglistGlobal or hookArglistLocal) + ('_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 '').. + 'if($_bllua_hook_abort)return $_bllua_hook_return;else ' or '').. ((hk.after and '%result=' or 'return ').. 'parent::'..name:match('[^:]+$').. '('..arglist..');') or '' @@ -841,10 +870,11 @@ local function updateHook(pkg, name, hk) arglist..');') or '' local code = 'package '..pkg..'{'.. - 'function '..name..'('..hookArglistLocal..'){'.. + 'function '..name..'('..tsArgsLocal..'){'.. beforeCode..parentCode..afterCode.. '}'.. '};' + print('bl.hook eval output: [['..code..']]') _bllua_ts.eval(code) end function bl.hook(pkg, name, time, func) @@ -870,9 +900,9 @@ 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' then + 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 @@ -899,10 +929,13 @@ function bl.unhook(pkg, name, time) 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 tableEmpty(bl._hooks[pkg][name]) and tableEmpty(bl._hooks[pkg]) then + if tableEmpty(bl._hooks[pkg][name]) then + bl._hooks[pkg][name] = nil + end + if tableEmpty(bl._hooks[pkg]) then bl._hooks[pkg] = nil - deactivatePackage(pkg) updateHook(pkg, name, {}) + deactivatePackage(pkg) else updateHook(pkg, name, bl._hooks[pkg][name]) end @@ -958,7 +991,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 @@ -1007,7 +1040,7 @@ end local maxTsArgLen = 8192 local function valsToString(vals) local strs = {} - for i,v in ipairs(vals) do + for i,v in ipairsNilable(vals) do local tstr = table.tostring(v) if #tstr>maxTsArgLen then tstr = tostring(v) diff --git a/src/util/libts-lua.lua b/src/util/libts-lua.lua index ebf3d29..3cee43b 100644 --- a/src/util/libts-lua.lua +++ b/src/util/libts-lua.lua @@ -93,11 +93,15 @@ 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 @@ -142,9 +146,9 @@ end ---@diagnostic disable-next-line: duplicate-set-field function io.type(f) ----@diagnostic disable-next-line: undefined-field + ---@diagnostic disable-next-line: undefined-field if type(f) == 'table' and f._is_file then ----@diagnostic disable-next-line: undefined-field + ---@diagnostic disable-next-line: undefined-field return f._is_open and 'file' or 'closed file' else return _bllua_io_type(f) @@ -183,14 +187,14 @@ 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 .. '.lua', -- local file './' .. fp .. '/init.lua', -- local library - fp .. '.lua', -- global file - fp .. '/init.lua', -- global 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 .. '.lua') -- add-on file table.insert(fns, addonpath .. fp .. '/init.lua') -- add-on library end for _, fn in ipairs(fns) do diff --git a/src/util/std.lua b/src/util/std.lua index ce57486..1774162 100644 --- a/src/util/std.lua +++ b/src/util/std.lua @@ -13,8 +13,19 @@ 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 @@ -30,6 +41,17 @@ function table.map_list(f, ...) 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 = {} @@ -193,7 +215,7 @@ valueToString = function(v, tabLevel, seen) return tostring(v) else --error('table.tostring: table contains a '..t..' value, cannot serialize') - return 'nil --[[ cannot serialize ' .. tostring(v) .. ' ]]' + return 'nil --[[ ' .. tostring(v) .. ' ]]' end end function table.tostring(t)