1
0
forked from redo/BlockLua

Compare commits

..

16 Commits

12 changed files with 1222 additions and 1044 deletions

Binary file not shown.

View File

@@ -5,13 +5,13 @@ project(BlockLua CXX)
# Export compile_commands.json for VSCode IntelliSense
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Output directories to mirror compile.bat's build folder
# Output directories to mirror build.bat's build folder
set(OUTPUT_DIR ${CMAKE_SOURCE_DIR}/build)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_DIR})
# Global compile options to mirror compile.bat
# Global build options to mirror build.bat
add_compile_options(
-Wall
-Werror
@@ -27,14 +27,14 @@ include_directories(
${CMAKE_SOURCE_DIR}/inc/lua
)
# Link directories (for -L.) and libraries from compile.bat
# Link directories (for -L.) and libraries from build.bat
link_directories(
${CMAKE_SOURCE_DIR}
)
# Safe DLL
add_library(BlockLua SHARED src/bllua4.cpp)
# Ensure output name matches compile.bat
# Ensure output name matches build.bat
set_target_properties(BlockLua PROPERTIES OUTPUT_NAME "BlockLua")
# Linker flags and libraries
if(MSVC)

View File

@@ -1,7 +1,7 @@
@echo off
cd /d %~dp0
REM Ensure MinGW32 toolchain is first in PATH (matches compile.bat)
REM Ensure MinGW32 toolchain is first in PATH
set "PATH=C:\msys64\mingw32\bin;%PATH%"
REM Configure CMake (generate into build/)

View File

@@ -1,13 +0,0 @@
@echo off
cd /d %~dp0
set "PATH=C:\msys64\mingw32\bin;%PATH%"
if not exist build mkdir build
set buildargs=-Wall -Werror -m32 -shared -Isrc -Iinc/tsfuncs -Iinc/lua -lpsapi -L. -llua5.1 -static-libgcc -static-libstdc++
echo on
g++ src/bllua4.cpp %buildargs% -o build\BlockLua.dll
g++ -DBLLUA_UNSAFE src/bllua4.cpp %buildargs% -o build\BlockLua-Unsafe.dll
@echo off

View File

@@ -27,7 +27,7 @@ What these packages are for:
- Run the script:
```powershell
compile.bat
build.bat
```
What the script does:
@@ -48,5 +48,5 @@ You should see `architecture: i386` in the output.
### Notes
- Ensure you installed the i686 (32-bit) variants of the packages; x86_64 packages wont work for a 32-bit build.
- Ensure you installed the i686 (32-bit) variants of the packages; x86_64 packages won't work for a 32-bit build.
- If the linker cannot find `-llua5.1`, confirm `mingw-w64-i686-lua51` is installed and you are using the `mingw32` toolchain (not `x86_64`).

View File

@@ -1,25 +1,26 @@
# BlockLua
Lua scripting for Blockland
## How to Install
- Install RedBlocklandLoader
- Copy `lua5.1.dll` into your Blockland install folder, next to `Blockland.exe`
- Copy `BlockLua.dll` into the `modules` folder within the Blockland folder
## Quick Reference
### 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
`bl.funcName(args)` - Call a TorqueScript function
`bl.varName` - Read a TorqueScript global variable
@@ -28,6 +29,7 @@ Lua scripting for Blockland
`bl['namespaceName::funcName'](args)` - Call a namespaced TorqueScript function
### Accessing Torque Objects from Lua
`bl.objectName` - Access a Torque object by name
`bl[objectID]` - Access a Torque object by ID (or name)
`object.fieldOrKey` - Read a field or Lua key from a Torque object
@@ -35,28 +37,43 @@ 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
### Timing/Schedules
`sched = bl.schedule(timeMs, function, args...)` - Schedule a Lua function to be called later, similar to schedule in Torque
`sched:cancel()` - Cancel a previously scheduled timer
### Raycasts and Searches
`hitObject, hitPos, hitNormal = bl.raycast(vector{startPosX,y,z}, vector{endPosX,y,z}, 'objtype'/{'objtypes',...}, ignoreObjects...?)` - Cast a ray in the world over objects of the specified type(s) (possibly excluding some objects), and return the object hit, the position of the hit, and the normal vector to the surface hit. See the Types section for a list of valid object types.
`for object in bl.boxSearch(vector{centerX,y,z}, vector{sizeX,y,z}, 'objtype'/{'objtypes',...}) do` - Find all objects in the world of the specified type(s) whose bounding box overlaps with the specified box. See the Types section for a list of valid object types.
`for object in bl.radiusSearch(vector{centerX,y,z}, radius, 'objtype'/{'objtypes',...}) do` - Find all objects of the specified type(s) whose bounding box overlaps with the specified sphere. See the Types section for a list of valid object types.
### 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.
@@ -65,10 +82,12 @@ In an `after` hook, `args._return` is set to the value returned by the parent fu
`bl.unhook('packageName')` - Remove any previously defined hooks within the package
### Modules and Dependencies
`dofile('Add-Ons/Path/file.lua')` - Execute a Lua file. Relative paths (`./file.lua`) are allowed. `..` is not allowed.
`require('modulePath.moduleName')` - Load a Lua file or external library.
`require` replaces `.` with `/` in the path, and then searches for files in the following order:
- `./modulePath/moduleName.lua`
- `./modulePath/moduleName/init.lua`
- `modulePath/moduleName.lua` (Relative to game directory)
@@ -80,6 +99,7 @@ In an `after` hook, `args._return` is set to the value returned by the parent fu
Like in standard Lua, modules loaded using `require` are only executed the first time `require` is called with that path (from anywhere). Subsequent calls simply return the result from the initial execution. To allow hot reloading, use `dofile`.
### File I/O
Lua's builtin file I/O is emulated, and is confined to the same directories as TorqueScript file I/O.
Relative paths (`./`) are allowed. `..` is not allowed.
`file = io.open('./file.txt', 'r'/'w'/'a'/'rb'/'wb'/'ab')` - Open a file
@@ -90,6 +110,7 @@ Reading files from ZIPs is supported, with caveats. Null characters are not allo
When reading from outside ZIPs, binary files are fully supported.
### Object Creation
`bl.new('className')` - Create a new Torque object
`bl.new('className', {fieldName = value, ...})` - Create a new Torque object with the given fields
`bl.new('className objectName', fields?)` - Create a new named Torque object
@@ -98,6 +119,7 @@ When reading from outside ZIPs, binary files are fully supported.
`bl.datablock('datablockClassName datablockName:parentDatablockName', fields?)` - Create a new datablock with inheritance
### Classes and Types
`bl.type('varName', 'type')` - Register the type of a Torque global variable, for conversion when accessing from Lua. Valid types are 'boolean', 'object', 'string' (prevents automatic conversion), and nil (default, applies automatic conversion).
`bl.type('funcName', 'type')` - Register the return type of a Torque function, for conversion when calling from Lua. Valid types are 'bool', 'object', and nil - all other conversion is automatic. Already done for all default functions.
`bl.type('className::funcName', 'type')` - Register the return type of a Torque object method.
@@ -108,6 +130,7 @@ When reading from outside ZIPs, binary files are fully supported.
`bl.string(arg)` - Manually convert any automatically-converted Torque value back into a string. This is not as reliable as using `bl.type` to specify the type as a string beforehand.
### Vector
`vec = vector{x,y,z}` - Create a vector. Can have any number of elements
`vec1 + vec2` - Add
`vec1 - vec2` - Subtract
@@ -133,17 +156,21 @@ When reading from outside ZIPs, binary files are fully supported.
`vec2 = vec:copy()` - Clone a vector so its elements can be modified without affecting the original. Usually not needed - the builtin vector functions never modify vectors in-place.
### Matrix
WIP
### Extended Standard Lua Library
`string[index]`
`string[{start,stop}]`
`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)`
@@ -167,49 +194,47 @@ WIP
`math.clamp(num, min, max)`
## Type Conversion
When a TorqueScript function is called from Lua or vice-versa, the arguments and return value must be converted between the two languages' type systems.
TorqueScript stores no type information; all values in TorqueScript are strings. So it's necessary to make some inferences when converting values between the two languages.
### From 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'`
### 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.
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`
With any _32-bit_ variant of GCC installed (such as MinGW or MSYS2), run the following command in the repo directory:
`g++ src/bllua4.cpp -o BlockLua.dll -m32 -shared -static-libgcc -Isrc -Iinc/tsfuncs -Iinc/lua -lpsapi -L. -llua5.1`
LuaJIT (lua5.1.dll) can be obtained from https://luajit.org/

View File

@@ -1,4 +1,4 @@
// BlockLua (bllua4): Simple Lua interface for TorqueScript
// BlockLua (bllua4): Advanced Lua interface for TorqueScript
// Includes
@@ -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);

View File

@@ -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,
@@ -64,6 +62,9 @@ local disallowed_exts = tmap {
-- Return: clean file path if allowed (or nil if disallowed),
-- error string (or nil if allowed)
local function safe_path(fn, readonly)
if type(fn) ~= 'string' then
return nil, 'Filename must be a string'
end
fn = fn:gsub('\\', '/')
fn = fn:gsub('^ +', '')
fn = fn:gsub(' +$', '')
@@ -81,14 +82,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 +122,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

View File

@@ -18,6 +18,8 @@ end
-- Called when pcall fails on a ts->lua call, used to print detailed error info
function _bllua_on_error(err)
-- Convert error to string if it's not already
err = tostring(err)
err = err:match(': (.+)$') or err
local tracelines = { err }
local level = 2
@@ -25,7 +27,7 @@ function _bllua_on_error(err)
local info = debug.getinfo(level)
if not info then break end
local filename = debug.getfilename(level) or info.short_src
local funcname = info.name
local funcname = info.name or '<unknown>'
if funcname == 'dofile' then break end
table.insert(tracelines, string.format('%s:%s in function \'%s\'',
filename,
@@ -37,5 +39,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')

View File

@@ -6,15 +6,33 @@ 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
return u
local i = 0
return function()
i = i + 1
if i > maxk then
return nil
else
return i, t[i]
end
end
end
-- Validation
local function isValidFuncName(name)
return type(name) == 'string' and name:find('^[a-zA-Z_][a-zA-Z0-9_]*$')
end
@@ -107,27 +125,34 @@ 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]+)$')
if xS then return vector{tonumber(xS),tonumber(yS),tonumber(zS)}
else return nil end
if xS then
return vector { tonumber(xS), tonumber(yS), tonumber(zS) }
else
return nil
end
end
local function boxFromTs(val)
local x1S, y1S, z1S, x2S, y2S, z2S = val:match(
'^(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+) ' ..
'(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$')
if x1S then return {
if x1S then
return {
vector { tonumber(x1S), tonumber(y1S), tonumber(z1S) },
vector { tonumber(x2S), tonumber(y2S), tonumber(z2S) } }
else return nil end
else
return nil
end
end
local function multinumericFromTs(val)
local tsNumPat = '%-?[0-9]+%.?[0-9]*e?[0-9]*'
@@ -154,9 +179,11 @@ 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
error('valFromTs: expected string, got ' .. type(val), 3)
end
if name then
name = name:lower()
if bl._forceType[name] then
@@ -177,31 +204,42 @@ 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) == '\001' 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_]+)(::.+)$')
if not class then
class, rest = name:match('^([a-zA-Z0-9_]+)(%..+)$') end
class, rest = name:match('^([a-zA-Z0-9_]+)(%..+)$')
end
return class, rest
end
local setForceType
setForceType = function(ftname, typ)
if typ ~= nil and not fromTsForceTypes[typ] then
error('bl.type: invalid type \''..typ..'\'', 2) end
error('bl.type: invalid type \'' .. typ .. '\'', 2)
end
if not isValidFuncNameNsArgn(ftname) then
error('bl.type: invalid function or variable name \''..ftname..'\'', 2) end
error('bl.type: invalid function or variable name \'' .. ftname .. '\'', 2)
end
ftname = ftname:lower()
@@ -252,15 +290,19 @@ local function tsIsFunction(name) return tsBool(_bllua_ts.call('isFunction', nam
local function tsIsFunctionNs(ns, name) return tsBool(_bllua_ts.call('isFunction', ns, name)) end
local function tsIsFunctionNsname(nsname)
local ns, name = nsname:match('^([^:]+)::([^:]+)$')
if ns then return tsIsFunctionNs(ns, name)
else return tsIsFunction(nsname) end
if ns then
return tsIsFunctionNs(ns, name)
else
return tsIsFunction(nsname)
end
end
-- sanity check to make sure objects that don't isObject are always marked as ._deleted
-- can be removed later
local function assertTsObjectExists(obj)
local is = tsIsObject(obj._tsObjectId)
if not is then
print('Warning: TS object :exists or isobject from lua but no longer exists') end
print('Warning: TS object :exists or isobject from lua but no longer exists')
end
return is
end
function bl.isObject(obj)
@@ -278,6 +320,7 @@ function bl.isObject(obj)
error('bl.isObject: argument #1: expected torque object, number, or string', 2)
end
end
function bl.isFunction(name)
return tsIsFunctionNsname(name)
end
@@ -292,9 +335,11 @@ local tsClassMeta = {
bl._objectUserMetas = bl._objectUserMetas or {}
function bl.class(cname, inhname)
if not (type(cname) == 'string' and isValidFuncName(cname)) then
error('bl.class: argument #1: invalid class name', 2) end
error('bl.class: argument #1: invalid class name', 2)
end
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
error('bl.class: argument #2: inherit name must be a string or nil', 2)
end
cname = cname:lower()
local met = bl._objectUserMetas[cname] or {
@@ -309,15 +354,18 @@ function bl.class(cname, inhname)
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
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
'inherits a different parent.', 2)
end
met._inherit = inh
-- apply inherited method and field types
@@ -329,6 +377,7 @@ function bl.class(cname, inhname)
end
end
end
local function objectInheritedMetas(name)
local inh = bl._objectUserMetas[name:lower()]
return function()
@@ -343,15 +392,18 @@ local tsObjectMeta = {
-- Return torque member function or value
__index = function(t, name)
if rawget(t, '_deleted') then
error('ts object index: object no longer exists', 2) end
error('ts object index: object no longer exists', 2)
end
if type(name) ~= 'string' and type(name) ~= 'number' then
error('ts object index: index must be a string or number', 2) end
error('ts object index: index must be a string or number', 2)
end
if getmetatable(t)[name] then
return getmetatable(t)[name]
elseif type(name) == 'number' then
if not tsIsFunctionNs(rawget(t, '_tsNamespace'), 'getObject') then
error('ts object __index: index is number, but object does not have ' ..
'getObject method', 2) end
'getObject method', 2)
end
return toTsObject(_bllua_ts.callobj(t._tsObjectId, 'getObject',
tostring(name)))
else
@@ -362,17 +414,20 @@ 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
@@ -383,9 +438,11 @@ local tsObjectMeta = {
-- Use :set() to set Torque data
__newindex = function(t, name, val)
if rawget(t, '_deleted') then
error('ts object newindex: object no longer exists', 2) end
error('ts object newindex: object no longer exists', 2)
end
if type(name) ~= 'string' then
error('ts object newindex: index must be a string', 2) end
error('ts object newindex: index must be a string', 2)
end
rawset(t, name, val)
-- create strong reference since it's now storing lua data
bl._objectsStrong[rawget(t, '_tsObjectId')] = t
@@ -394,11 +451,14 @@ local tsObjectMeta = {
-- Use to set torque data
set = function(t, name, val)
if t == nil or type(t) ~= 'table' or not t._tsObjectId then
error('ts object method: be sure to use :func() not .func()', 2) end
error('ts object method: be sure to use :func() not .func()', 2)
end
if t._deleted then
error('ts object method: object no longer exists', 2) end
error('ts object method: object no longer exists', 2)
end
if type(name) ~= 'string' then
error('ts object :set(): index must be a string', 2) end
error('ts object :set(): index must be a string', 2)
end
_bllua_ts.setfield(t._tsObjectId, name, valToTs(val))
end,
-- __tostring: Called when printing
@@ -412,9 +472,11 @@ local tsObjectMeta = {
-- If the object has a getCount method, return its count
__len = function(t)
if t._deleted then
error('ts object __len: object no longer exists', 2) end
error('ts object __len: object no longer exists', 2)
end
if not tsIsFunctionNs(t._tsNamespace, 'getCount') then
error('ts object __len: object has no getCount method', 2) end
error('ts object __len: object has no getCount method', 2)
end
return tonumber(_bllua_ts.callobj(t._tsObjectId, 'getCount'))
end,
-- object:members()
@@ -422,14 +484,17 @@ local tsObjectMeta = {
-- for index, object in group:members() do ... end
members = function(t)
if t == nil then
error('ts object method: be sure to use :func() not .func()', 2) end
error('ts object method: be sure to use :func() not .func()', 2)
end
if t._deleted then
error('ts object method: object no longer exists', 2) end
error('ts object method: object no longer exists', 2)
end
if not (
tsIsFunctionNs(t._tsNamespace, 'getCount') and
tsIsFunctionNs(t._tsNamespace, 'getObject')) then
error('ts object :members() - ' ..
'Object does not have getCount and getObject methods', 2) end
'Object does not have getCount and getObject methods', 2)
end
local count = tonumber(_bllua_ts.callobj(t._tsObjectId, 'getCount'))
local idx = 0
return function()
@@ -437,7 +502,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
@@ -446,23 +512,29 @@ local tsObjectMeta = {
-- Wrap some Torque functions for performance and error checking
getName = function(t)
if t == nil then
error('ts object method: be sure to use :func() not .func()', 2) end
error('ts object method: be sure to use :func() not .func()', 2)
end
if t._deleted then
error('ts object method: object no longer exists', 2) end
error('ts object method: object no longer exists', 2)
end
return t._tsName
end,
getId = function(t)
if t == nil then
error('ts object method: be sure to use :func() not .func()', 2) end
error('ts object method: be sure to use :func() not .func()', 2)
end
if t._deleted then
error('ts object method: object no longer exists', 2) end
error('ts object method: object no longer exists', 2)
end
return tonumber(t._tsObjectId)
end,
getType = function(t)
if t == nil then
error('ts object method: be sure to use :func() not .func()', 2) end
error('ts object method: be sure to use :func() not .func()', 2)
end
if t._deleted then
error('ts object method: object no longer exists', 2) end
error('ts object method: object no longer exists', 2)
end
return tsTypesByNum[_bllua_ts.callobj(t._tsObjectId, 'getType')]
end,
---- Schedule method for objects
@@ -484,7 +556,8 @@ local tsObjectMeta = {
--end,
exists = function(t)
if t == nil then
error('ts object method: be sure to use :func() not .func()', 2) end
error('ts object method: be sure to use :func() not .func()', 2)
end
if t._deleted then
return false
else
@@ -518,13 +591,15 @@ end
-- Return a Torque object for the object ID string, or create one if none exists
toTsObject = function(idiS)
if type(idiS) ~= 'string' then
error('toTsObject: input must be a string', 2) end
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
return nil
end
local className = _bllua_ts.callobj(idiS, 'getClassName')
local obj = {
@@ -544,14 +619,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 +633,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 +661,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,
@@ -612,16 +688,22 @@ function bl.call(func, ...)
local argsS = arglistToTs(args)
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 = { ... }
return name .. table.concat(rest, '_')
end
function bl.boolean(val)
return val ~= nil and
val ~= false and
@@ -629,6 +711,7 @@ function bl.boolean(val)
--val~='0' and
val ~= 0
end
function bl.object(id)
if type(id) == 'table' and id._tsObjectId then
return id
@@ -638,6 +721,7 @@ function bl.object(id)
error('bl.object: id must be a ts object, number, or string', 2)
end
end
function bl.string(val)
return valFromTsTostring(val)
end
@@ -645,26 +729,36 @@ 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 set then tbl[first] = {}
else return nil end
error('luaLookup: invalid name \'' .. tostring(first) .. '\'', 3)
end
if tbl[first] == nil then
if set then
tbl[first] = {}
else
return nil
end
end
return luaLookup(tbl[first], rest, set, val)
elseif name:find(':') then
local first, rest = name:match('^([^%.:]+):(.*)$')
if rest:find('[%.:]') then
error('luacall: cannot have : or . after :', 3) end
error('luacall: cannot have : or . after :', 3)
end
if not isValidFuncName(first) then
error('luacall: invalid name \''..tostring(first)..'\'', 3) end
error('luacall: invalid name \'' .. tostring(first) .. '\'', 3)
end
if not isValidFuncName(rest) then
error('luacall: invalid method name \'' .. tostring(first) .. '\'', 3)
end
if not tbl[first] then
error('luacall: no object named \''..rest..'\'', 3) end
if not tbl[first][rest] then
error('luacall: no method named \''..rest..'\'', 3) end
error('luacall: no object named \'' .. rest .. '\'', 3)
end
if tbl[first][rest] == nil then
error('luacall: no method named \'' .. rest .. '\'', 3)
end
return function(...)
tbl[first][rest](tbl[first], ...)
end
@@ -681,19 +775,23 @@ 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
function _bllua_getvar(vname)
return valToTs(luaLookup(_G, vname))
end
function _bllua_setvar(vname, valS)
return valToTs(luaLookup(_G, vname, true, valFromTs(valS, vname))) -- todo: separate lua from ts var names?
end
function _bllua_eval(code) return loadstring(code)() end
function _bllua_exec(fn) return dofile(fn, 2) end
function _bllua_eval(code) return loadstring(code)() end
function _bllua_exec(fn) return dofile(fn, 2) end
-- bl.schedule: Use TS's schedule function to schedule lua calls
-- bl.schedule(time, function, args...)
@@ -721,6 +819,7 @@ function bl.schedule(time, cb, ...)
bl._scheduleTable[id] = sch
return sch
end
function _bllua_schedule_callback(idS)
local id = tonumber(idS) or
error('_bllua_schedule_callback: invalid id: ' .. tostring(idS))
@@ -739,19 +838,21 @@ function _bllua_process_cmd(cmdS, ...)
if not func then error('_bllua_process_cmd: no cmd named \'' .. cmd .. '\'') end
func(unpack(args)) --pcall(func, unpack(args))
end
local function addCmd(cmd, func)
if not isValidFuncName(cmd) then
error('addCmd: invalid function name \''..tostring(cmd)..'\'') end
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()
addCmd('servercmd' .. name, func)
bl._forceType['servercmd' .. name .. ':1'] = 'object'
end
function bl.addClientCmd(name, func)
name = name:lower()
addCmd('clientcmd' .. name, func)
@@ -763,11 +864,14 @@ 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
function bl.commandToAll(cmd, ...)
_bllua_ts.call('commandToAll',
_bllua_ts.call('addTaggedString', cmd),
@@ -793,26 +897,25 @@ 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, { ... })
local func = bl._hooks[pkgS] and bl._hooks[pkgS][nameS] and
bl._hooks[pkgS][nameS].before
if not func then
error('_bllua_process_hook_before: no hook for '..pkgs..':'..nameS) end
error('_bllua_process_hook_before: no hook for ' .. pkgs .. ':' .. nameS)
end
_bllua_ts.setvar('_bllua_hook_abort', '0')
func(args) --pcall(func, args)
if args._return then
_bllua_ts.setvar('_bllua_hook_abort', '1')
_bllua_ts.setvar('_bllua_hook_return', valToTs(args._return))
end
for i=1,hookNargs do
for i = 1, tsMaxArgs do
_bllua_ts.setvar('_bllua_hook_arg' .. i, valToTs(args[i]))
end
end
function _bllua_process_hook_after(pkgS, nameS, resultS, ...)
nameS = nameS:lower()
local args = arglistFromTs(nameS, { ... })
@@ -820,15 +923,17 @@ function _bllua_process_hook_after(pkgS, nameS, resultS, ...)
local func = bl._hooks[pkgS] and bl._hooks[pkgS][nameS] and
bl._hooks[pkgS][nameS].after
if not func then
error('_bllua_process_hook_after: no hook for '..pkgs..':'..nameS) end
error('_bllua_process_hook_after: no hook for ' .. pkgs .. ':' .. nameS)
end
func(args) --pcall(func, args)
return valToTs(args._return)
end
local function updateHook(pkg, name, hk)
local beforeCode = hk.before and
('_bllua_luacall("_bllua_process_hook_before","' .. pkg .. '","' .. name ..
'",'..hookArglistLocal..');') or ''
local arglist = (hk.before and hookArglistGlobal or hookArglistLocal)
'",' .. tsArgsLocal .. ');') or ''
local arglist = (hk.before and tsArgsGlobal or tsArgsLocal)
local parentCode =
tsIsFunctionNsname(name) and -- only call parent if it exists
(hk.before and
@@ -841,21 +946,26 @@ 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)
if not isValidFuncName(pkg) then
error('bl.hook: argument #1: invalid package name \''..tostring(pkg)..'\'', 2) end
error('bl.hook: argument #1: invalid package name \'' .. tostring(pkg) .. '\'', 2)
end
if not isValidFuncNameNs(name) then
error('bl.hook: argument #2: invalid function name \''..tostring(name)..'\'', 2) end
error('bl.hook: argument #2: invalid function name \'' .. tostring(name) .. '\'', 2)
end
if time ~= 'before' and time ~= 'after' then
error('bl.hook: argument #3: time must be \'before\' or \'after\'', 2) end
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
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 {}
@@ -864,16 +974,20 @@ function bl.hook(pkg, name, time, func)
updateHook(pkg, name, bl._hooks[pkg][name])
activatePackage(pkg)
end
local function tableEmpty(t)
return next(t) ~= nil
end
function bl.unhook(pkg, name, time)
if not isValidFuncName(pkg) then
error('bl.unhook: argument #1: invalid package name \''..tostring(pkg)..'\'', 2) end
if not isValidFuncNameNs(name) then
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
error('bl.unhook: argument #1: invalid package name \'' .. tostring(pkg) .. '\'', 2)
end
if name and not isValidFuncNameNs(name) then
error('bl.unhook: argument #2: invalid function name \'' .. tostring(name) .. '\'', 2)
end
if time and time ~= 'before' and time ~= 'after' then
error('bl.unhook: argument #3: time must be \'before\' or \'after\'', 2)
end
if not name then
if bl._hooks[pkg] then
@@ -897,12 +1011,16 @@ function bl.unhook(pkg, name, time)
updateHook(pkg, name, {})
else
if time ~= 'before' and time ~= 'after' then
error('bl.unhook: argument #3: time must be nil, \'before\', or \'after\'', 2) end
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
@@ -917,14 +1035,16 @@ end
-- Container search/raycast
local function vecToTs(v)
if not isTsVector(v) then
error('vecToTs: argument is not a vector', 3) end
error('vecToTs: argument is not a vector', 3)
end
return table.concat(v, ' ')
end
local function maskToTs(mask)
if type(mask) == 'string' then
local val = tsTypesByName[mask:lower()]
if not val then
error('maskToTs: invalid mask \''..mask..'\'', 3) end
error('maskToTs: invalid mask \'' .. mask .. '\'', 3)
end
return tostring(val)
elseif type(mask) == 'table' then
local tval = 0
@@ -934,7 +1054,8 @@ local function maskToTs(mask)
local val = tsTypesByName[v:lower()]
if not val then
error('maskToTs: invalid mask \'' .. v ..
'\' at index '..i..' in mask list', 3) end
'\' at index ' .. i .. ' in mask list', 3)
end
tval = tval + val
seen[v] = true
end
@@ -958,7 +1079,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
@@ -976,6 +1097,7 @@ function bl.raycast(start, stop, mask, ignores)
return hit, pos, norm
end
end
local function tsContainerSearchIterator()
local retS = _bllua_ts.call('containerSearchNext')
if retS == '0' then
@@ -992,10 +1114,12 @@ function bl.boxSearch(pos, size, mask)
_bllua_ts.call('initContainerBoxSearch', posS, sizeS, maskS)
return tsContainerSearchIterator
end
function bl.radiusSearch(pos, radius, mask)
local posS = vecToTs(pos)
if type(radius) ~= 'number' then
error('bl.radiusSearch: argument #2: radius must be a number', 2) end
error('bl.radiusSearch: argument #2: radius must be a number', 2)
end
local radiusS = tostring(radius)
local maskS = maskToTs(mask)
@@ -1007,7 +1131,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)
@@ -1033,7 +1157,8 @@ local function createTsObj(keyword, class, name, inherit, props)
if props then
for k, v in pairs(props) do
if not isValidFuncName(k) then
error('bl.new/bl.datablock: invalid property name \''..k..'\'') end
error('bl.new/bl.datablock: invalid property name \'' .. k .. '\'')
end
table.insert(propsT, k .. '="' .. valToTs(v) .. '";')
end
end
@@ -1044,7 +1169,8 @@ local function createTsObj(keyword, class, name, inherit, props)
table.concat(propsT) .. '};')
local obj = toTsObject(objS)
if not obj then
error('bl.new/bl.datablock: failed to create object', 3) end
error('bl.new/bl.datablock: failed to create object', 3)
end
return obj
end
@@ -1069,13 +1195,15 @@ local function parseTsDecl(decl)
(inherit == nil or isValidFuncName(inherit))) then
error('bl.new/bl.datablock: invalid decl \'' .. decl .. '\'\n' ..
'must be of the format: \'className\', \'className name\', ' ..
'\'className :inherit\', or \'className name:inherit\'', 3) end
'\'className :inherit\', or \'className name:inherit\'', 3)
end
return class, name, inherit
end
function bl.new(decl, props)
local class, name, inherit = parseTsDecl(decl)
return createTsObj('new', class, name, inherit, props)
end
function bl.datablock(decl, props)
local class, name, inherit = parseTsDecl(decl)
if not name then error('bl.datablock: must specify a name', 2) end

View File

@@ -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

View File

@@ -4,7 +4,7 @@
-- 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
@@ -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)