diff --git a/BlockLua.dll b/BlockLua.dll index 58d6e45..a11bb18 100644 Binary files a/BlockLua.dll and b/BlockLua.dll differ diff --git a/readme.md b/readme.md index 1e5e5c2..e78f471 100644 --- a/readme.md +++ b/readme.md @@ -36,6 +36,8 @@ Lua scripting for Blockland `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. +`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 @@ -101,10 +103,11 @@ When reading from outside ZIPs, binary files are fully supported. `bl.type('className::funcName', 'type')` - Register the return type of a Torque object method. `bl.class('className')` - Register an existing Torque class to be used from Lua. Already done for all built-in classes. `bl.class('className', 'parentClassName')` - Same as above, with inheritance -`bl.boolean(thing)` - Manually convert a Torque boolean (0 or 1) into a Lua boolean. -`bl.object(thing)` - Manually convert a Torque object reference (object ID or name) into a Lua object. +`bl.boolean(arg)` - Manually convert a Torque boolean (0 or 1) into a Lua boolean. +`bl.object(arg)` - Manually convert a Torque object reference (object ID or name) into a Lua object. +`bl.string(arg)` - Manually convert any automatically-converted Torque value back into a string. This is not as reliable as using `bl.type` to specify the type as a string beforehand. -### Vectors +### Vector `vec = vector{x,y,z}` - Create a vector. Can have any number of elements `vec1 + vec2` - Add `vec1 - vec2` - Subtract @@ -129,6 +132,9 @@ When reading from outside ZIPs, binary files are fully supported. `vec1:distance(vec2)` - Distance between two points `vec2 = vec:copy()` - Clone a vector so its elements can be modified without affecting the original. Usually not needed - the builtin vector functions never modify vectors in-place. +### Matrix +WIP + ### Extended Standard Lua Library `string[index]` `string[{start,stop}]` @@ -163,12 +169,7 @@ When reading from outside ZIPs, binary files are fully supported. ## 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 TorqueScript to Lua -- Any numeric value becomes a Lua `number`, except as specified with `bl.type`, which may convert a value into a `boolean` or a Torque object container. -- The empty string "" becomes `nil` -- A string containing three numbers separated by spaces becomes a `vector` -- A string containing six numbers separated by spaces becomes a table of two vectors -- Any other string is passed directly as a `string` + ### From Lua to TorqueScript - `nil` becomes the empty string "" - `true` and `false` become "1" and "0" respectively @@ -178,6 +179,17 @@ TorqueScript stores no type information; all values in TorqueScript are strings. - Any `string` is passed directly as a string - Tables cannot be passed and will throw an error +### From TorqueScript to Lua +- Any numeric value becomes a Lua `number`, except as specified with `bl.type`, which may convert a value into a `boolean` or a Torque object container. +- The empty string "" becomes `nil` +- A string containing two or three numbers separated by single spaces becomes a `vector` +- A string containing six numbers separated by single spaces becomes a table of two vectors, usually defining the corners a bounding box +- (WIP) A string containing seven numbers separated by single spaces is treated as an axis-angle (a "transform" in TorqueScript parlance), and is converted into a `matrix` representing the translation and rotation. +- Any other string is passed directly as a `string` + +For scenarios where the automatic TorqueScript->Lua conversion rules are insufficient or incorrect, use `bl.type`. +To convert objects by hand, use `bl.object`, `bl.boolean`, or `bl.string`. + ## I/O and Safety 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. diff --git a/src/bllua4.cpp b/src/bllua4.cpp index c8955fe..5e1d94b 100644 --- a/src/bllua4.cpp +++ b/src/bllua4.cpp @@ -28,6 +28,7 @@ INCLUDE_BIN(bll_fileLuaEnv , "lua-env.lua"); INCLUDE_BIN(bll_fileTsEnv , "ts-env.cs" ); INCLUDE_BIN(bll_fileLuaStd , "util/std.lua"); INCLUDE_BIN(bll_fileLuaVector , "util/vector.lua"); +INCLUDE_BIN(bll_fileLuaMatrix , "util/matrix.lua"); INCLUDE_BIN(bll_fileLuaLibts , "util/libts-lua.lua"); INCLUDE_BIN(bll_fileTsLibts , "util/libts-ts.cs"); INCLUDE_BIN(bll_fileLuaLibbl , "util/libbl.lua" ); @@ -67,6 +68,7 @@ bool init() { // 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); diff --git a/src/lua-env-safe.lua b/src/lua-env-safe.lua index c64a30e..adb5275 100644 --- a/src/lua-env-safe.lua +++ b/src/lua-env-safe.lua @@ -139,4 +139,5 @@ debug = { getfilename = old_debug.getfilename, -- defined in lua.env.lua } -_bllua_ts.echo(' Executed bllua-env-safe.lua') +print = _bllua_ts.echo +print(' Executed bllua-env-safe.lua') diff --git a/src/lua-env.lua b/src/lua-env.lua index f485ee2..e16f7c2 100644 --- a/src/lua-env.lua +++ b/src/lua-env.lua @@ -37,4 +37,5 @@ function _bllua_on_error(err) return table.concat(tracelines, '\n') end -_bllua_ts.echo(' Executed bllua-env.lua') +print = _bllua_ts.echo +print(' Executed bllua-env.lua') diff --git a/src/util/libbl-support.cs b/src/util/libbl-support.cs index c4032d0..873b50b 100644 --- a/src/util/libbl-support.cs +++ b/src/util/libbl-support.cs @@ -42,4 +42,26 @@ package _bllua_objectDeletionHook { }; activatePackage(_bllua_objectDeletionHook); +// Public Lua library for TS +function luacall(%func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p) { + if($_bllua_active) + return _bllua_luacall("_bllua_call", %func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p); +} +function luaexec(%fn) { + if($_bllua_active) + return _bllua_luacall("_bllua_exec", %fn); +} +function luaeval(%code) { + if($_bllua_active) + return _bllua_luacall("_bllua_eval", %code); +} +function luaget(%name) { + if($_bllua_active) + return _bllua_luacall("_bllua_getvar", %name); +} +function luaset(%name, %val) { + if($_bllua_active) + _bllua_luacall("_bllua_setvar", %name, %val); +} + echo(" Executed libbl-support.cs"); diff --git a/src/util/libbl.lua b/src/util/libbl.lua index efd5889..52195df 100644 --- a/src/util/libbl.lua +++ b/src/util/libbl.lua @@ -2,8 +2,6 @@ -- Main lua-side functionality of bllua, -- provided through the global table 'bl.' --- todo: set - local _bllua_ts = ts bl = bl or {} @@ -18,7 +16,7 @@ local function map(t,f) return u end local function isValidFuncName(name) - return type(name)=='string' and name:find('^[a-zA-Z0-9_]+$') + return type(name)=='string' and name:find('^[a-zA-Z_][a-zA-Z0-9_]*$') end local function isValidFuncNameNs(name) return type(name)=='string' and ( @@ -76,7 +74,7 @@ for k,v in pairs(tsTypesByName) do tsTypesByNum[v] = k end --- Type conversion +-- Type conversion from Lua to TS local toTsObject -- Convert a string from TS into a boolean -- Note: Nonempty nonnumeric strings evaluate to 1, unlike in TS @@ -100,21 +98,61 @@ local function valToTs(val) -- box - > 6 numbers return table.concat(val[1], ' ')..' '..table.concat(val[2], ' ') else - error('valToTs: could not convert table', 3) + error('valToTs: cannot pass Lua tables to TorqueScript.', 3) end else - error('valToTs: could not convert '..type(val), 3) + error('valToTs: could not convert value to TorqueScript: '..tostring(val), 3) end end + +-- Type conversion from TS to Lua local fromTsForceTypes = { ['boolean'] = tsBool, - ['object'] = function(val) toTsObject(val) end, -- toTsObject not defined yet - ['string'] = tostring, + ['object'] = function(val) toTsObject(val) end, -- wrap because toTsObject not defined yet + ['string'] = function(val) return val end, } -local function convertValFromTs(val, typ) +local function forceValFromTs(val, typ) return fromTsForceTypes[typ](val) or error('valFromTs: invalid force type '..typ, 4) 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 +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 { + vector{tonumber(x1S),tonumber(y1S),tonumber(z1S)}, + vector{tonumber(x2S),tonumber(y2S),tonumber(z2S)} } + else return nil end +end +local function multinumericFromTs(val) + local tsNumPat = '%-?[0-9]+%.?[0-9]*e?[0-9]*' + if val:find('^'..tsNumPat) then + local nums = {} + for _,part in ipairs(val:split(' ')) do + if part:find('^'..tsNumPat..'$') then + table.insert(nums, tonumber(part)) + else + return nil + end + end + if #nums==2 or #nums==3 then + return vector(nums) + elseif #nums==6 then -- box + return { + vector{nums[1], nums[2], nums[3]}, + vector{nums[4], nums[5], nums[6]} } + elseif #nums==7 then -- axis + return nil --return matrix(nums) + else + return nil + end + end +end bl._forceType = bl._forceType or {} local function valFromTs(val, name, name2) -- todo: ensure name and name2 are already lowercase if type(val)~='string' then @@ -122,13 +160,13 @@ local function valFromTs(val, name, name2) -- todo: ensure name and name2 are al if name then name = name:lower() if bl._forceType[name] then - return convertValFromTs(val, bl._forceType[name]) + return forceValFromTs(val, bl._forceType[name]) end end if name2 then name2 = name2:lower() if bl._forceType[name2] then - return convertValFromTs(val, bl._forceType[name2]) + return forceValFromTs(val, bl._forceType[name2]) end end -- '' -> nil @@ -136,16 +174,9 @@ local function valFromTs(val, name, name2) -- todo: ensure name and name2 are al -- number local num = tonumber(val) if num then return num end - -- vector - 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)} end - 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]+)$') - -- box (2 vectors) - if x1S then return { - vector{tonumber(x1S),tonumber(y1S),tonumber(z1S)}, - vector{tonumber(x2S),tonumber(y2S),tonumber(z2S)} } end + -- vector, box, or axis->matrix + local vec = multinumericFromTs(val) + if vec then return vec end -- string return val end @@ -189,13 +220,33 @@ setForceType = function(ftname, typ) end bl.type = setForceType +-- Type conversion TS->Lua backwards, convert back to string +local function numFromTsTostring(val) + -- todo: as-good-as-possible scientific notation for numbers + return tostring(val) +end +local function valFromTsTostring(val) + if type(val)=='string' then + return val + elseif type(val)=='number' then + return numFromTsTostring(val) + elseif type(val)=='table' then + if val.__is_vector then + local strs = {} + for i=1,#val do strs[i] = numFromTsTostring(val[i]) end + return table.concat(strs, ' ') + elseif val.__is_matrix then + -- todo: matrix back to axis-angle string + error('bl.string: matrix not yet supported', 3) + end + end + error('bl.string: cannot convert \''..type(val)..'\'', 3) +end --- Value detection - +-- Getting info from TS about functions and objects local function isTsObject(t) return type(t)=='table' and t._tsObjectId~=nil end - local function tsIsObject(name) return tsBool(_bllua_ts.call('isObject', name)) end local function tsIsFunction(name) return tsBool(_bllua_ts.call('isFunction', name)) end local function tsIsFunctionNs(ns, name) return tsBool(_bllua_ts.call('isFunction', ns, name)) end @@ -204,27 +255,31 @@ local function tsIsFunctionNsname(nsname) 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 + return is +end function bl.isObject(obj) - if isTsObject(obj) then - obj = obj._tsObjectId - elseif type(obj)=='number' then - obj = tostring(obj) - elseif type(obj)~='string' then + if type(obj)=='number' then -- object id + return tsIsObject(tostring(obj)) + elseif type(obj)=='string' then -- object name + return tsIsObject(obj) + elseif isTsObject(obj) then -- lua object + if obj._deleted then + return false + else + return assertTsObjectExists(obj) + end + else error('bl.isObject: argument #1: expected torque object, number, or string', 2) end - return tsIsObject(obj) end -function bl.isFunction(a1, a2) - if type(a1)~='string' then - error('bl.isFunction: argument #1: expected string', 2) end - if a2 then - if type(a2)~='string' then - error('bl.isFunction: argument #2: expected string', 2) end - return tsIsFunctionNs(a1, a2) - else - return tsIsFunction(a1) - end +function bl.isFunction(name) + return tsIsFunctionNsname(name) end -- Torque object pseudo-class @@ -350,7 +405,8 @@ local tsObjectMeta = { -- Display a nice info string __tostring = function(t) return 'torque:'..t._tsNamespace..':'..t._tsObjectId.. - (t._tsName~='' and ('('..t._tsName..')') or '') + (t._tsName~='' and ('('..t._tsName..')') or '').. + (t._deleted and '(deleted)' or '') end, -- #object -- If the object has a getCount method, return its count @@ -430,8 +486,10 @@ local tsObjectMeta = { if t==nil then error('ts object method: be sure to use :func() not .func()', 2) end if t._deleted then - return false end - return tsIsObject(t._tsObjectId) + return false + else + return assertTsObjectExists(t) + end end, } -- Weak-values table for caching Torque object references @@ -464,7 +522,7 @@ toTsObject = function(idiS) idiS = idiS:lower() if bl._objectsWeak[idiS] then return bl._objectsWeak[idiS] end - if not tsBool(_bllua_ts.call('isObject', idiS)) then + if not tsIsObject(idiS) then --error('toTsObject: object \''..idiS..'\' does not exist', 2) end return nil end @@ -560,6 +618,10 @@ end function bl.exec(file) return valFromTs(_bllua_ts.call('exec', file)) end +function bl.array(name, ...) + local rest = {...} + return name..table.concat(rest, '_') +end function bl.boolean(val) return val~=nil and val~=false and @@ -576,18 +638,62 @@ function bl.object(id) error('bl.object: id must be a ts object, number, or string', 2) end end -function bl.array(name, ...) - local rest = {...} - return name..table.concat(rest, '_') +function bl.string(val) + return valFromTsTostring(val) end -function _bllua_call(fnameS, ...) - local args = arglistFromTs('lua:'..fnameS:lower(), {...}) -- todo: allow this though bl.type - if not _G[fnameS] then - error('luacall: no global lua function named \''..fnameS..'\'') end - -- todo: library fields and object methods - local res = _G[fnameS](unpack(args)) + +-- 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 + 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 + if not isValidFuncName(first) then + error('luacall: invalid 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 + return function(...) + tbl[first][rest](tbl[first], ...) + end + else + if set then + tbl[name] = val + else + return tbl[name] + end + end +end +-- Todo: similar deep access for luaget and luaset +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 + 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 + -- bl.schedule: Use TS's schedule function to schedule lua calls -- bl.schedule(time, function, args...) diff --git a/src/util/libts-lua.lua b/src/util/libts-lua.lua index 96ef5fc..56f4a9d 100644 --- a/src/util/libts-lua.lua +++ b/src/util/libts-lua.lua @@ -192,12 +192,6 @@ function require(mod) return _bllua_requiresecure(mod) end --- Exposure to TS -function _bllua_getvar(name) return _G[name] end -function _bllua_setvar(name, val) _G[name] = val end -function _bllua_eval(code) return loadstring(code)() end -function _bllua_exec(fn) return dofile(fn, 2) end - local function isValidCode(code) local f,e = loadstring(code) return f~=nil diff --git a/src/util/libts-ts.cs b/src/util/libts-ts.cs index 292c40e..63e03a7 100644 --- a/src/util/libts-ts.cs +++ b/src/util/libts-ts.cs @@ -49,26 +49,4 @@ function _bllua_set_var(%name, %val) { return ""; } -// Public Lua library for TS -function luacall(%func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p) { - if($_bllua_active) - return _bllua_luacall("_bllua_call", %func, %a,%b,%c,%d,%e,%f,%g,%h,%i,%j,%k,%l,%m,%n,%o,%p); -} -function luaexec(%fn) { - if($_bllua_active) - return _bllua_luacall("_bllua_exec", %fn); -} -function luaeval(%code) { - if($_bllua_active) - return _bllua_luacall("_bllua_eval", %code); -} -function luaget(%name) { - if($_bllua_active) - return _bllua_luacall("_bllua_getvar", %name); -} -function luaset(%name, %val) { - if($_bllua_active) - _bllua_luacall("_bllua_setvar", %name, %val); -} - echo(" Executed libts-ts.cs"); diff --git a/src/util/matrix.lua b/src/util/matrix.lua new file mode 100644 index 0000000..41a3542 --- /dev/null +++ b/src/util/matrix.lua @@ -0,0 +1,7 @@ + +-- todo +-- Matrix class with math operators + + + +print(' Executed matrix.lua') diff --git a/src/util/std.lua b/src/util/std.lua index f1041cf..ca79eea 100644 --- a/src/util/std.lua +++ b/src/util/std.lua @@ -179,7 +179,7 @@ valueToString = function(v, tabLevel, seen) return tostring(v) else --error('table.tostring: table contains a '..t..' value, cannot serialize') - return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]' + return 'nil --[[ cannot serialize '..tostring(v)..' ]]' end end function table.tostring(t) @@ -345,3 +345,6 @@ end function math.clamp(v, n, x) return math.min(x, math.max(v, n)) end + + +print(' Executed std.lua') diff --git a/src/util/vector.lua b/src/util/vector.lua index f166719..bc92791 100644 --- a/src/util/vector.lua +++ b/src/util/vector.lua @@ -216,4 +216,7 @@ vector_new = function(vi) end vector = vector_new + +print(' Executed vector.lua') + return vector_new