This commit is contained in:
Redo
2025-10-01 16:26:18 -07:00
commit 36ba54b248
43 changed files with 9810 additions and 0 deletions

105
src/bllua4.cpp Normal file
View File

@@ -0,0 +1,105 @@
// BlockLua (bllua4): Simple Lua interface for TorqueScript
// Includes
#include <Windows.h>
#include <Psapi.h>
#include "lua.hpp"
#include "BlHooks.cpp"
#include "BlFuncs.cpp"
#include "luainterp.cpp"
#include "lualibts.cpp"
lua_State* gL;
#include "tsliblua.cpp"
// Global variables
// Setup
// Hack to encode the contents of text files as static strings
#define INCLUDE_BIN(varname, filename) \
asm("_" #varname ": .incbin \"" filename "\""); \
asm(".byte 0"); \
extern char varname[];
INCLUDE_BIN(bll_fileLuaEnvSafe, "lua-env-safe.lua");
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_fileLuaLibts , "util/libts.lua");
INCLUDE_BIN(bll_fileTsLibtsSupport, "util/libts-support.cs");
INCLUDE_BIN(bll_fileLuaLibbl , "util/libbl.lua" );
INCLUDE_BIN(bll_fileLuaLibblTypes , "util/libbl-types.lua");
INCLUDE_BIN(bll_fileTsLibblSupport, "util/libbl-support.cs");
INCLUDE_BIN(bll_fileLoadaddons , "util/loadaddons.cs");
#define BLL_LOAD_LUA(lstate, vname) \
if(!bll_LuaEval(lstate, vname)) { \
BlPrintf(" Error executing " #vname); \
return false; \
}
bool init() {
BlHooksInit();
BlPrintf("BlockLua: Loading");
BlFuncsInit();
// Initialize Lua environment
gL = lua_open();
luaL_openlibs(gL);
// Expose TS API to Lua
llibbl_init(gL);
// Set up Lua environment
BLL_LOAD_LUA(gL, bll_fileLuaEnv);
#ifndef BLLUA_UNSAFE
BLL_LOAD_LUA(gL, bll_fileLuaEnvSafe);
#endif
// 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_fileLuaLibts);
BlEval(bll_fileTsLibtsSupport);
BLL_LOAD_LUA(gL, bll_fileLuaLibbl);
BLL_LOAD_LUA(gL, bll_fileLuaLibblTypes);
BlEval(bll_fileTsLibblSupport);
BlEval(bll_fileLoadaddons);
BlEval("$_bllua_active = 1;");
BlPrintf(" BlockLua: Done Loading");
return true;
}
bool deinit() {
BlPrintf("BlockLua: Unloading");
BlEval("deactivatePackage(_bllua_main);");
BlEval("$_bllua_active = 0;");
bll_LuaEval(gL, "for _,f in pairs(_bllua_on_unload) do f() end");
lua_close(gL);
BlHooksDeinit();
BlFuncsDeinit();
BlPrintf(" BlockLua: Done Unloading");
return true;
}
bool __stdcall DllMain(HINSTANCE hinstance, DWORD reason, void* reserved) {
switch(reason) {
case DLL_PROCESS_ATTACH: return init();
case DLL_PROCESS_DETACH: return deinit();
default : return true;
}
}

142
src/lua-env-safe.lua Normal file
View File

@@ -0,0 +1,142 @@
-- Sanitize the Lua environment to:
-- Prevent scripts from accessing files outside the game directory
-- Prevent usage of libraries other than
-- Utility: Convert a list of strings into a map of string->true
local function tmap(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end
-- Save banned global variables for wrapping with safe functions
local old_io = io
local old_require = require
local old_os = os
local old_debug = debug
local old_package = package
-- Remove all global variables except a whitelist
local ok_names = tmap {
'_G', '_bllua_ts', '_bllua_on_unload', '_bllua_on_error',
'string', 'table', 'math', 'coroutine', 'bit',
'pairs', 'ipairs', 'next', 'unpack', 'select',
'error', 'assert', 'pcall', 'xpcall',
'type', 'tostring', 'tonumber',
'loadstring',
'getmetatable', 'setmetatable',
'rawget', 'rawset', 'rawequal', 'rawlen',
'module', '_VERSION',
}
local not_ok_names = {}
for n, _ in pairs(_G) do
if not ok_names[n] then
table.insert(not_ok_names, n)
end
end
for _, n in ipairs(not_ok_names) do
_G[n] = nil
end
-- Sanitize file paths to point only to allowed files within the game directory
-- List of allowed directories for reading/writing
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,
-- so this is just a precaution.
local disallowed_exts = tmap {
-- windows
'bat','bin','cab','cmd','com','cpl','ex_','exe','gadget','inf','ins','inx','isu',
'job','jse','lnk','msc','msi','msp','mst','paf','pif','ps1','reg','rgs','scr',
'sct','shb','shs','u3p','vb','vbe','vbs','vbscript','ws','wsf','wsh',
-- linux
'csh','ksh','out','run','sh',
-- mac/other
'action','apk','app','command','ipa','osx','prg','workflow',
}
-- Arguments: file name (relative to game directory), boolean true if only reading
-- Return: clean file path if allowed (or nil if disallowed),
-- error string (or nil if allowed)
local function safe_path(fn, readonly)
fn = fn:gsub('\\', '/')
fn = fn:gsub('^ +', '')
fn = fn:gsub(' +$', '')
-- whitelist characters
local ic = fn:find('[^a-zA-Z0-9_%-/ %.]')
if ic then
return nil, 'Filename \''..fn..'\' contains invalid character \''..
fn:sub(ic, ic)..'\' at position '..ic
end
-- disallow up-dirs, absolute paths, and relative paths
-- './' and '../' are possible in scripts, because they're processed into
-- absolute paths in util.lua before reaching here
if fn:find('^%.') or fn:find('%.%.') or fn:find(':') or fn:find('^/') then
return nil, 'Filename \''..fn..'\' contains invalid sequence'
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')
end
-- disallow blacklisted extensions or no extension
local ext = fn:match('%.([^/%.]+)$')
if (not ext) or (disallowed_exts[ext:lower()]) then
return nil, 'Filename \''..fn..'\' has disallowed extension \''..
(ext or '')..'\''
end
return fn, nil
end
-- Wrap io.open with path sanitization
function _bllua_io_open(fn, md)
md = md or 'r'
local readonly = md=='r' or md=='rb'
local fns, err = safe_path(fn, readonly)
if fns then
return old_io.open(fns, md)
else
return nil, err
end
end
-- Allow io.type (works on file handles returned by io.open)
function _bllua_io_type(f)
return old_io.type(f)
end
-- Wrap require with a blacklist for unsafe built-in modules
-- List of allowed packages to require
-- Note that util.lua wraps this and provides 'require',
-- only falling back here if the package is not found in user files
local disallowed_packages = tmap {
'ffi', 'debug', 'package', 'io', 'os',
'_bllua_ts',
}
function _bllua_requiresecure(name)
if name:find('[^a-zA-Z0-9_%-%.]') or name:find('%.%.') or
name:find('^%.') or name:find('%.$') then
error('require: package name contains invalid character', 3)
elseif disallowed_packages[name] then
error('require: attempt to require disallowed module \''..name..'\'', 3)
else
-- todo: reimplement require to not use package.* stuff?
return old_require(name)
end
end
package = {
seeall = old_package.seeall,
}
-- Provide limited debug
debug = {
traceback = old_debug.traceback,
getinfo = old_debug.getinfo,
getfilename = old_debug.getfilename, -- defined in lua.env.lua
}
_bllua_ts.echo(' Executed bllua-env-safe.lua')

40
src/lua-env.lua Normal file
View File

@@ -0,0 +1,40 @@
-- Set up the Lua environment
-- Point old require to modules/lua in game directory
package.path = 'modules\\lualib\\?.lua;modules\\lualib\\?\\init.lua;'
package.cpath = 'modules\\lualib\\?.dll;'
-- Set up table of unload/cleanup callbacks
_bllua_on_unload = {}
-- Utility for getting the current filename
function debug.getfilename(level)
if type(level) == 'number' then level = level+1 end
local info = debug.getinfo(level)
if not info then return nil end
local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]')
return filename
end
-- Called when pcall fails on a ts->lua call, used to print detailed error info
function _bllua_on_error(err)
err = err:match(': (.+)$') or err
local tracelines = {err}
local level = 2
while true do
local info = debug.getinfo(level)
if not info then break end
local filename = debug.getfilename(level) or info.short_src
local funcname = info.name
if funcname=='dofile' then break end
table.insert(tracelines, string.format('%s:%s in function \'%s\'',
filename,
info.currentline==-1 and '' or info.currentline..':',
funcname
))
level = level+1
end
return table.concat(tracelines, '\n')
end
_bllua_ts.echo(' Executed bllua-env.lua')

68
src/luainterp.cpp Normal file
View File

@@ -0,0 +1,68 @@
// Handle errors with a Lua function, defined in lua-env.lua
int bll_error_handler(lua_State *L) {
lua_getfield(L, LUA_GLOBALSINDEX, "_bllua_on_error");
if (!lua_isfunction(L, -1)) {
BlPrintf(" Lua error handler: _bllua_on_error not defined.");
lua_pop(L, 1);
return 1;
}
// move function to before arg
int hpos = lua_gettop(L) - 1;
lua_insert(L, hpos);
lua_pcall(L, 1, 1, 0);
return 1;
}
int bll_pcall(lua_State* L, int nargs, int nret) {
// calculate stack position for message handler
int hpos = lua_gettop(L) - nargs;
// push custom error message handler
lua_pushcfunction(L, bll_error_handler);
// move it before function and arguments
lua_insert(L, hpos);
// call lua_pcall function with custom handler
int ret = lua_pcall(L, nargs, nret, hpos);
// remove custom error message handler from stack
lua_remove(L, hpos);
return ret;
}
// Display the last Lua error in the BL console
void bll_printError(lua_State* L, const char* operation, const char* item) {
//error_handler(L);
BlPrintf("\x03Lua error: %s", lua_tostring(L, -1));
BlPrintf("\x03 (%s: %s)", operation, item);
lua_pop(L, 1);
}
// Eval a string of Lua code
bool bll_LuaEval(lua_State* L, const char* str) {
if(luaL_loadbuffer(L, str, strlen(str), "input") || bll_pcall(L, 0, 1)) {
bll_printError(L, "eval", str);
return false;
}
return true;
}
// Convert a Lua stack entry into a string for providing to TS
// Use static buffer to avoid excessive malloc
#define BLL_ARG_COUNT 20
#define BLL_ARG_MAX 8192
char bll_arg_buffer[BLL_ARG_COUNT][BLL_ARG_MAX];
bool bll_toarg(lua_State* L, char* buf, int i, bool err) {
if(lua_isstring(L, i)) {
const char* str = lua_tostring(L, i);
if(strlen(str) >= BLL_ARG_MAX) {
if(err) luaL_error(L, "argument to TS is too long - max length is 8192");
return true;
} else {
strcpy(buf, str);
return false;
}
} else {
if(err) luaL_error(L, "argument to TS must be a string");
return true;
}
}

159
src/lualibts.cpp Normal file
View File

@@ -0,0 +1,159 @@
//run ../compile
// TS -> Lua API
// Call a TS function from Lua, push the result to Lua stack
int bll_TsCall(lua_State* L, const char* oname, const char* fname, int argc, int ofs) {
ADDR obj = (ADDR)NULL;
if(oname) {
obj = BlObject(oname);
if(!obj) { return luaL_error(L, "Lua->TS call: Object not found"); }
}
if(argc > BLL_ARG_COUNT) { return luaL_error(L, "Lua->TS call: Too many arguments (Max is 19)"); }
char* argv[BLL_ARG_COUNT];
for(int i=0; i<argc; i++) {
char* argbuf = bll_arg_buffer[i];
argv[i] = argbuf;
bll_toarg(L, argbuf, i+ofs+1, true);
}
// /:^| /
const char* res;
if(obj) {
switch(argc) {
case 0: res = BlCallObj(obj, fname); break; // no idea why this happens sometimes, it shouldnt be possible
case 1: res = BlCallObj(obj, fname); break;
case 2: res = BlCallObj(obj, fname, argv[0]); break;
case 3: res = BlCallObj(obj, fname, argv[0], argv[1]); break;
case 4: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2]); break;
case 5: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3]); break;
case 6: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4]); break;
case 7: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]); break;
case 8: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6]); break;
case 9: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7]); break;
case 10: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8]); break;
case 11: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9]); break;
case 12: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10]); break;
case 13: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11]); break;
case 14: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12]); break;
case 15: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13]); break;
case 16: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14]); break;
case 17: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15]); break;
case 18: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15], argv[16]); break;
case 19: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15], argv[16], argv[17]); break;
case 20: res = BlCallObj(obj, fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15], argv[16], argv[17], argv[18]); break;
default: res = ""; luaL_error(L, "Lua->TS object call: Too many arguments (Max is 19)");
}
} else {
switch(argc) {
case 0: res = BlCall(fname); break;
case 1: res = BlCall(fname, argv[0]); break;
case 2: res = BlCall(fname, argv[0], argv[1]); break;
case 3: res = BlCall(fname, argv[0], argv[1], argv[2]); break;
case 4: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3]); break;
case 5: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4]); break;
case 6: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]); break;
case 7: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6]); break;
case 8: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7]); break;
case 9: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8]); break;
case 10: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9]); break;
case 11: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10]); break;
case 12: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11]); break;
case 13: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12]); break;
case 14: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13]); break;
case 15: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14]); break;
case 16: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15]); break;
case 17: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15], argv[16]); break;
case 18: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15], argv[16], argv[17]); break;
case 19: res = BlCall(fname, argv[0], argv[1], argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], argv[10], argv[11], argv[12], argv[13], argv[14], argv[15], argv[16], argv[17], argv[18]); break;
default: res = ""; luaL_error(L, "Lua->TS call: Too many arguments (Max is 19)");
}
}
lua_pushstring(L, res);
return 1;
}
// Lua lib function: ts.call
int bll_lua_tscall(lua_State* L) {
int argc = lua_gettop(L)-1; // number of arguments after function name
if(argc < 0) return luaL_error(L, "_bllua_ts.call: Must provide a function name");
const char* fname = luaL_checkstring(L, 1);
return bll_TsCall(L, NULL, fname, argc, 1);
}
// Lua lib function: ts.callobj
int bll_lua_tscallobj(lua_State* L) {
int argc = lua_gettop(L)-2; // number of arguments after function name and object?
if(argc < 0) return luaL_error(L, "_bllua_ts.callobj: Must provide an object and function name");
const char* oname = luaL_checkstring(L, 1);
const char* fname = luaL_checkstring(L, 2);
return bll_TsCall(L, oname, fname, argc, 2);
}
// Lua lib function: ts.getvar
int bll_lua_tsgetvar(lua_State* L) {
const char* vname = luaL_checkstring(L, 1);
const char* var = BlGetVar(vname);
lua_pushstring(L, var);
return 1;
}
// Lua lib function: ts.getfield
int bll_lua_tsgetfield(lua_State* L) {
const char* oname = luaL_checkstring(L, 1);
const char* vname = luaL_checkstring(L, 2);
ADDR obj = BlObject(oname);
if(!obj) { return luaL_error(L, "_bllua_ts.getfield: Object not found"); }
const char* val = BlGetField(obj, vname, NULL);
lua_pushstring(L, val);
return 1;
}
// Lua lib function: ts.setfield
int bll_lua_tssetfield(lua_State* L) {
const char* oname = luaL_checkstring(L, 1);
const char* vname = luaL_checkstring(L, 2);
const char* val = luaL_checkstring(L, 3);
ADDR obj = BlObject(oname);
if(!obj) { return luaL_error(L, "_bllua_ts.setfield: Object not found"); }
BlSetField(obj, vname, NULL, val);
return 0;
}
// Lua lib function: ts.eval
int bll_lua_tseval(lua_State* L) {
const char* str = luaL_checkstring(L, 1);
const char* res = BlEval(str);
lua_pushstring(L, res);
return 1;
}
// Lua lib function: ts.echo
// Print to BL console - used in Lua print implementation
int bll_lua_tsecho(lua_State* L) {
const char* str = luaL_checkstring(L, 1);
BlPrintf("%s", str);
return 0;
}
const luaL_Reg bll_lua_reg[] = {
{"call" , bll_lua_tscall },
{"callobj" , bll_lua_tscallobj },
{"getvar" , bll_lua_tsgetvar },
{"getfield", bll_lua_tsgetfield},
{"setfield", bll_lua_tssetfield},
{"eval" , bll_lua_tseval },
{"echo" , bll_lua_tsecho },
{NULL, NULL},
};
void llibbl_init(lua_State* L) {
luaL_register(L, "_bllua_ts", bll_lua_reg);
}

26
src/ts-env.cs Normal file
View File

@@ -0,0 +1,26 @@
// Built-in functions
// Eval'd after BLLua4 has loaded the Lua environment and API
// 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(%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 bllua-env.cs");

25
src/tsliblua.cpp Normal file
View File

@@ -0,0 +1,25 @@
// Call a Lua function from TS, return true if success - result will be on Lua stack
bool bll_LuaCall(const char* fname, int argc, const char* argv[]) {
lua_getglobal(gL, fname);
for(int i=0; i<argc; i++) {
lua_pushstring(gL, argv[i]);
}
if(bll_pcall(gL, argc, 1)) {
bll_printError(gL, "call", fname);
return false;
}
return true;
}
// TS lib function: luacall
const char* bll_ts_luacall(ADDR obj, int argc, const char* argv[]) {
if(argc<2) return "";
if(!bll_LuaCall(argv[1], argc-2, &argv[2])) { return ""; }
char* retbuf = BlReturnBuffer(BLL_ARG_MAX);
bll_toarg(gL, retbuf, -1, false); // provide returned value to ts
lua_pop(gL, 1); // pop returned value
return retbuf;
}

44
src/util/libbl-support.cs Normal file
View File

@@ -0,0 +1,44 @@
// Private - Utilities used by libbl from the Lua side
package _bllua_smartEval {
// Allow prepending ' to console commands to eval in lua instead of TS
// Also wraps TS lines with echo(...); if they don't end in ; or }
function ConsoleEntry::eval() {
%text = ConsoleEntry.getValue();
if(getSubStr(%text, 0, 1)$="\'") { // lua eval
if($_bllua_active) {
%text = getSubStr(%text, 1, strLen(%text));
echo("Lua ==> " @ %text);
luacall("_bllua_smarteval", %text);
} else {
echo("Lua: not loaded");
}
ConsoleEntry.setValue("");
} else {
%textT = trim(%text);
if(strLen(%textT)>0) {
%lastChar = getSubStr(%textT, strLen(%textT)-1, 1);
if(%lastChar!$=";" && %lastChar!$="}") {
%text = "echo(" @ %text @ ");";
ConsoleEntry.setValue(%text);
}
}
parent::eval();
}
}
};
activatePackage(_bllua_smartEval);
package _bllua_objectDeletionHook {
// Hook object deletion to clean up its lua data
function SimObject::onRemove(%obj) {
// note: no parent function exists by default,
// and this is loaded before any addons
//parent::onRemove(%obj);
if($_bllua_active) luacall("_bllua_objectDeleted", %obj);
}
};
activatePackage(_bllua_objectDeletionHook);
echo(" Executed libbl-support.cs");

3706
src/util/libbl-types.lua Normal file

File diff suppressed because it is too large Load Diff

840
src/util/libbl.lua Normal file
View File

@@ -0,0 +1,840 @@
-- bl library
-- Main lua-side functionality of bllua,
-- provided through the global table 'bl.'
-- todo: set
local _bllua_ts = ts
bl = bl or {}
-- 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)
end
return u
end
local function isValidFuncName(name)
return type(name)=='string' and name:find('^[a-zA-Z0-9_]+$')
end
local function isValidFuncNameNs(name)
return type(name)=='string' and (
name:find('^[a-zA-Z0-9_]+$') or
name:find('^[a-zA-Z0-9_]+::[a-zA-Z0-9_]+$') )
end
local function isValidFuncNameNsArgn(name)
return type(name)=='string' and (
name:find('^[a-zA-Z0-9_]+$') or
name:find('^[a-zA-Z0-9_]+%.[a-zA-Z0-9_]+$') or
name:find('^[a-zA-Z0-9_]+::[a-zA-Z0-9_]+$') or
name:find('^[a-zA-Z0-9_]+:[0-9]+$') or
name:find('^[a-zA-Z0-9_]+::[a-zA-Z0-9_]+:[0-9]+$') )
end
-- Whether a var can be converted into a TS vector
local function isTsVector(val)
if type(val)~='table' then return false end
if #val~=3 and #val~=2 then return false end
if val.__is_vector then return true end
for _,v in ipairs(val) do
if type(v)~='number' then return false end
end
return true
end
-- Use strings for object types instead of integer bitmasks like in TS
local tsTypesByName = {
['all'] = -1,
['static'] = 1,
['environment'] = 2,
['terrain'] = 4,
['water'] = 16,
['trigger'] = 32,
['marker'] = 64,
['gamebase'] = 1024,
['shapebase'] = 2048,
['camera'] = 4096,
['staticshape'] = 8192,
['player'] = 16384,
['item'] = 32768,
['vehicle'] = 65536,
['vehicleblocker'] = 131072,
['projectile'] = 262144,
['explosion'] = 524288,
['corpse'] = 1048576,
['debris'] = 4194304,
['physicalzone'] = 8388608,
['staticts'] = 16777216,
['brick'] = 33554432,
['brickalways'] = 67108864,
['staticrendered'] = 134217728,
['damagableitem'] = 268435456,
}
local tsTypesByNum = {}
for k,v in pairs(tsTypesByName) do
tsTypesByNum[v] = k
end
-- Type conversion
local toTsObject
-- Convert a string from TS into a boolean
-- Note: Nonempty nonnumeric strings evaluate to 1, unlike in TS
local function tsBool(v) return v~='' and v~='0' end
-- Convert a Lua var into a TS string, or error if not possible
local function valToTs(val)
if val==nil then -- nil -> ''
return ''
elseif type(val)=='boolean' then -- bool -> 0 or 1
return val and '1' or '0'
elseif type(val)=='number' then -- number
return tostring(val)
elseif type(val)=='string' then -- string
return val
elseif type(val)=='table' then
if val._tsObjectId then -- object -> object id
return tostring(val._tsObjectId)
elseif isTsVector(val) then -- vector - > 3 numbers
return table.concat(val, ' ')
elseif #val==2 and isTsVector(val[1]) and isTsVector(val[2]) then
-- box - > 6 numbers
return table.concat(val[1], ' ')..' '..table.concat(val[2], ' ')
else
error('valToTs: could not convert table', 3)
end
else
error('valToTs: could not convert '..type(val), 3)
end
end
bl._forceType = bl._forceType or {}
local function valFromTs(val, name)
if type(val)~='string' then
error('valFromTs: expected string, got '..type(val), 3) end
if name then
local nameL = name:lower()
if bl._forceType[nameL] then
local typ = bl._forceType[nameL]
if typ=='boolean' then
return tsBool(val)
elseif typ=='object' then
return toTsObject(val)
else
error('valFromTs: invalid force type '..typ, 3)
end
end
end
-- '' -> nil
if val=='' then return nil end
-- 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
-- string
return val
end
local function arglistFromTs(name, argsS)
local args = {}
for i,arg in ipairs(argsS) do
args[i] = valFromTs(arg, name..':'..i)
end
return args
end
local function arglistToTs(args)
return map(args, valToTs)
end
function bl.type(name,typ)
if typ~='bool' and typ~='boolean' and typ~='object' and typ~=nil then
error('bl.type: can only set type to \'bool\' or \'object\' or nil', 2) end
if not isValidFuncNameNsArgn(name) then
error('bl.type: invalid function or variable name \''..name..'\'', 2) end
if typ=='bool' then typ='boolean' end
bl._forceType[name:lower()] = typ
end
-- Value detection
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
local function tsIsFunctionNsname(nsname)
local ns, name = nsname:match('^([^:]+)::([^:]+)$')
if ns then return tsIsFunctionNs(ns, name)
else return tsIsFunction(nsname) end
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
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
end
-- Torque object pseudo-class
bl._objectUserMetas = bl._objectUserMetas or {}
function bl.class(name, inherit)
if not ( type(name)=='string' and isValidFuncName(name) ) then
error('bl.class: argument #1: invalid class name', 2) end
if not ( inherit==nil or (type(inherit)=='string' and isValidFuncName(inherit)) ) then
error('bl.class: argument #2: inherit name must be a string or nil', 2) end
name = name:lower()
local met = bl._objectUserMetas[name] or {}
bl._objectUserMetas[name] = met
met._name = name
if inherit then
inherit = inherit:lower()
local inh = bl._objectUserMetas[inherit]
if not inh then error('bl.class: argument #2: \''..inherit..'\' is not the '..
'name of an existing class', 2) end
local inhI = bl._objectUserMetas[name]._inherit
if inhI and inhI~=inh then
error('bl.class: argument #2: class already exists and '..
'inherits a different parent.', 2) end
bl._objectUserMetas[name]._inherit = inh
end
end
local function objectInheritedMetas(name)
local inh = bl._objectUserMetas[name:lower()]
return function()
local inhP = inh
if inhP==nil then return nil end
inh = inh._inherit
return inhP
end
end
local tsObjectMeta = {
-- __index: Called when accessing fields that don't exist in the object itself
-- 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
if type(name)~='string' and type(name)~='number' then
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
return toTsObject(_bllua_ts.callobj(t._tsObjectId, 'getObject',
tostring(name)))
else
for inh in objectInheritedMetas(rawget(t,'_tsClassName')) do
if inh[name] then return inh[name] end
end
if tsIsFunctionNs(rawget(t,'_tsNamespace'), name) then
return function(t, ...)
local args = {...}
local argsS = arglistToTs(args)
return valFromTs(_bllua_ts.callobj(rawget(t,'_tsObjectId'), name, unpack(argsS)),
rawget(t,'_tsNamespace')..'::'..name)
end
else
return valFromTs(_bllua_ts.getfield(rawget(t,'_tsObjectId'), name),
rawget(t,'_tsNamespace')..'.'..name)
end
end
end,
-- __newindex: Called when setting fields on the object
-- Set lua data
-- 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
if type(name)~='string' then
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
end,
-- object:set(fieldName, value)
-- 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
if t._deleted then
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
_bllua_ts.setfield(t._tsObjectId, name, valToTs(val))
end,
-- __tostring: Called when printing
-- Display a nice info string
__tostring = function(t)
return 'torque:'..t._tsNamespace..':'..t._tsObjectId..
(t._tsName~='' and ('('..t._tsName..')') or '')
end,
-- #object
-- 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
if not tsIsFunctionNs(t._tsNamespace, 'getCount') then
error('ts object __len: object has no getCount method', 2) end
return tonumber(_bllua_ts.callobj(t._tsObjectId, 'getCount'))
end,
-- object:iterate()
-- Return an iterator for Torque objects with the getCount and getObject methods
-- for index, object in group:iterate() do ... end
iterate = function(t)
if t==nil then
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
if not (
tsIsFunctionNs(t._tsNamespace, 'getCount' ) and
tsIsFunctionNs(t._tsNamespace, 'getObject')) then
error('ts object :iterate() - '..
'Object does not have getCount and getObject methods', 2) end
local count = tonumber(_bllua_ts.callobj(t._tsObjectId, 'getCount'))
local idx = 0
return function()
if idx < count then
local obj = toTsObject(_bllua_ts.callobj(t._tsObjectId,
'getObject', tostring(idx)))
idx = idx+1
return idx-1, obj
else
return nil
end
end
end,
-- 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
if t._deleted then
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
if t._deleted then
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
if t._deleted then
error('ts object method: object no longer exists', 2) end
return tsTypesByNum[_bllua_ts.callobj(t._tsObjectId, 'getType')]
end,
---- Schedule method for objects
--schedule = function(t, time, cb, ...)
-- if type(t)~='table' or not t._tsObjectId then
-- 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
-- if type(time)~='number' then
-- error('ts object schedule: argument #2: time must be a number', 2) end
-- if type(cb)~='function' then
-- error('ts object schedule: argument #3: callback must be a function', 2) end
-- local args = {...}
-- bl.schedule(time, function()
-- if tsBool(__bllua_ts.call('isObject', t._tsObjectId)) then
-- pcall(cb, unpack(args))
-- end
-- end)
--end,
exists = function(t)
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)
end,
}
-- Weak-values table for caching Torque object references
-- Objects in this table can be garbage collected if there are no other refs to them
if not bl._objectsWeak then
bl._objectsWeak = {}
setmetatable(bl._objectsWeak, { __mode='v' })
end
-- Strong table for preserving Torque object references containing lua data
-- If an object in this table, it will remain here and in the Weak table until deleted
if not bl._objectsStrong then
bl._objectsStrong = {}
end
-- Hook object deletion to clean up its lua data
-- idS is expected to be the object ID number, NOT the object name
function _bllua_objectDeleted(idS)
local obj = bl._objectsWeak[idS]
if obj then
obj._deleted = true
bl._objectsStrong[idS] = nil
bl._objectsWeak[idS] = nil
bl._objectsWeak[obj._tsName:lower()] = nil
end
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
idiS = idiS:lower()
if bl._objectsWeak[idiS] then return bl._objectsWeak[idiS] end
if not tsBool(_bllua_ts.call('isObject', idiS)) then
--error('toTsObject: object \''..idiS..'\' does not exist', 2) end
return nil end
local className = _bllua_ts.callobj(idiS, 'getClassName')
local obj = {
_tsObjectId = _bllua_ts.callobj(idiS, 'getId' ),
_tsName = _bllua_ts.callobj(idiS, 'getName' ),
_tsNamespace = className,
_tsClassName = className:lower()
}
setmetatable(obj, tsObjectMeta)
bl._objectsWeak[obj._tsObjectId ] = obj
bl._objectsWeak[obj._tsName:lower()] = obj
return obj
end
-- Metatable for the global bl library
-- Allows accessing Torque objects, variables, and functions by indexing it
local tsMeta = {
-- __index: Called when accessing fields that don't exist in the table itself
-- Allow indexing by object id: bl[1234]
-- by object name: bl.mainMenuGui
-- by function name: bl.quit()
-- by variable name: bl.iAmAdmin
__index = function(t, name)
if getmetatable(t)[name] then
return getmetatable(t)[name]
elseif bl._objectUserMetas[name:lower()] then
return bl._objectUserMetas[name:lower()]
else
if type(name)=='number' then
return toTsObject(tostring(name))
elseif name:find('::') then
local ns, rest = name:match('^([^:]+)::(.+)$')
if not ns then error('ts index: invalid name \''..name..'\'', 2) end
if not rest:find('::') and tsIsFunction(ns, rest) then
error('ts index: can\'t call a namespaced function from lua', 2)
else
return valFromTs(_bllua_ts.getvar(name), name)
end
elseif tsIsFunction(name) then
return function(...)
local args = {...}
local argsS = arglistToTs(args)
return valFromTs(_bllua_ts.call(name, unpack(argsS)), name)
end
elseif tsIsObject(name) then
return toTsObject(name)
else
return valFromTs(_bllua_ts.getvar(name), name)
end
end
end,
}
-- bl.set(name, value)
-- Used to set global variables
function bl.set(t, name, val)
_bllua_ts.call('_bllua_set_var', name, valToTs(val))
end
-- Utility functions
function bl.call(func, ...)
local args = {...}
local argsS = arglistToTs(args)
return _bllua_ts.call(func, unpack(argsS))
end
function bl.eval(code)
return valFromTs(_bllua_ts.call('eval', code))
end
function bl.exec(file)
return valFromTs(_bllua_ts.call('exec', file))
end
function bl.tobool(val)
return val~=nil and
val~=false and
--val~='' and
--val~='0' and
val~=0
end
function bl.toobject(id)
if type(id)=='table' and id._tsObjectId then
return id
elseif type(id)=='string' or type(id)=='number' then
return toTsObject(tostring(id))
else
error('bl.toobject: id must be a ts object, number, or string', 2)
end
end
function bl.array(name, ...)
local rest = {...}
return name..table.concat(rest, '_')
end
function _bllua_call(name, ...)
-- todo: call ts->lua using this instead of directly
end
-- bl.schedule: Use TS's schedule function to schedule lua calls
-- bl.schedule(time, function, args...)
bl._scheduleTable = bl._scheduleTable or {}
bl._scheduleNextId = bl._scheduleNextId or 1
local function cancelTsSched(sched)
if not (sched and sched.handle) then
error('schedule:cancel() - invalid object', 2)
end
_bllua_ts.call('cancel', sched.handle)
bl._scheduleTable[id] = nil
end
function bl.schedule(time, cb, ...)
local id = bl._scheduleNextId
bl._scheduleNextId = bl._scheduleNextId+1
local args = {...}
local handle = tonumber(_bllua_ts.call('schedule',
time, 0, 'luacall', '_bllua_schedule_callback', id))
local sch = {
callback = cb,
args = args,
handle = handle,
cancel = cancelTsSched,
}
bl._scheduleTable[id] = sch
return sch
end
function _bllua_schedule_callback(id)
id = tonumber(id)
local sch = bl._scheduleTable[id]
if not sch then error('_ts_schedule_callback: no schedule with id '..id) end
bl._scheduleTable[sched_id] = nil
sch.callback(unpack(sch.args))
end
-- serverCmd and clientCmd
-- bl.serverCmd('suicide', function(client) client.player:kill() end)
bl._cmds = bl._cmds or {}
function _bllua_process_cmd(cmdS, clientS, ...)
local client = toTsObject(clientS)
local argsS = {...}
local args = arglistFromTs(cmdS, argsS)
local func = bl._cmds[cmdS]
if not func then error('_bllua_process_cmd: no cmd named \''..cmd..'\'') end
pcall(func, client, unpack(args))
end
local function addCmd(cmd, func)
if not isValidFuncName(cmd) then
error('addCmd: invalid function name \''..tostring(cmd)..'\'') end
bl._servercmds[cmd] = func
local arglist = '%a,%b,%c,%d,%e,%f,%g,%h'
_bllua_ts.eval('function '..cmd..'(%cl,'..arglist..'){'..
'luacall(_bllua_process_cmd,"'..cmd..'",%cl,'..arglist..');}')
end
function bl.serverCmd(name, func) addCmd('serverCmd'..name, func) end
function bl.clientCmd(name, func) addCmd('clientCmd'..name, func) end
-- Hooks (using TS packages)
local function isPackageActive(pkg)
local numpkgs = tonumber(_bllua_ts.call('getNumActivePackages'))
for i = 0, numpkgs-1 do
local apkg = _bllua_ts.call('getActivePackage', tostring(i))
if apkg==pkg then return true end
end
return false
end
local function activatePackage(pkg)
if not isPackageActive(pkg) then
_bllua_ts.call('activatePackage', pkg)
end
end
local function deactivatePackage(pkg)
if isPackageActive(pkg) then
_bllua_ts.call('deactivatePackage', pkg)
end
end
bl._hooks = bl._hooks or {}
function _bllua_process_hook(pkgS, nameS, timeS, ...)
local argsS = {...}
local args = arglistFromTs(nameS, argsS)
local func = bl._hooks[pkgS] and bl._hooks[pkgS][nameS] and
bl._hooks[pkgS][nameS][timeS]
if not func then
error('_bllua_process_hook: no hook for '..pkgS..':'..nameS..':'..timeS) end
pcall(func, args)
end
local function updateHook(pkg, name, hk)
local arglist = '%a,%b,%c,%d,%e,%f,%g,%h'
local beforeCode = hk.before and
('luacall("_bllua_process_hook", "'..pkg..'", "'..name..
'", "before", '..arglist..');') or ''
local parentCode = hk.override and
('luacall("_bllua_process_hook", "'..pkg..'", "'..name..
'", "override", '..arglist..');') or
(tsIsFunctionNsname(name) and
('parent::'..name:match('[^:]+$')..'('..arglist..');') or '')
local afterCode = hk.after and
('luacall("_bllua_process_hook", "'..pkg..'", "'..name..
'", "after", '..arglist..');') or ''
bl.eval('package '..pkg..'{function '..name..'('..arglist..'){'..
beforeCode..parentCode..afterCode..'}};')
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
if not isValidFuncNameNs(name) then
error('bl.hook: argument #2: invalid function name \''..tostring(name)..'\'', 2) end
if time~='before' and time~='after' and time~='override' then
error('bl.hook: argument #3: time must be one of '..
'\'before\' \'after\' \'override\'', 2) end
if type(func)~='function' then
error('bl.hook: argument #4: expected a function', 2) end
bl._hooks[pkg] = bl._hooks[pkg] or {}
bl._hooks[pkg][name] = bl._hooks[pkg][name] or {}
bl._hooks[pkg][name][time] = func
updateHook(pkg, name, bl._hooks[pkg][name])
activatePackage(pkg)
end
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' and time~='override' then
error('bl.unhook: argument #3: time must be one of '..
'\'before\' \'after\' \'override\'', 2) end
if not name then
if bl._hooks[pkg] then
for name,hk in pairs(bl._hooks[pkg]) do
updateHook(pkg, name, {})
end
bl._hooks[pkg] = nil
else
--error('bl.unhook: no hooks registered under package name \''..
-- pkg..'\'', 2)
end
deactivatePackage(pkg)
else
if bl._hooks[pkg][name] then
if not time then
bl._hooks[pkg][name] = nil
if table.isempty(bl._hooks[pkg]) then
bl._hooks[pkg] = nil
deactivatePackage(pkg)
end
updateHook(pkg, name, {})
else
if time~='before' and time~='after' and time~='override' then
error('bl.unhook: argument #3: time must be nil or one of '..
'\'before\' \'after\' \'override\'', 2) end
bl._hooks[pkg][name][time] = nil
if table.isempty(bl._hooks[pkg][name]) and table.empty(bl._hooks[pkg]) then
bl._hooks[pkg] = nil
deactivatePackage(pkg)
end
updateHook(pkg, name, bl._hooks[pkg][name])
end
else
--error('bl.unhook: no hooks registered for function \''..name..
-- '\' under package name \''..pkg..'\'', 2)
end
end
end
-- Container search/raycast
local function vecToTs(v)
if not isTsVector(v) then
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
return tostring(val)
elseif type(mask)=='table' then
local tval = 0
local seen = {}
for i,v in ipairs(mask) do
if not seen[v] then
local val = tsTypesByName[v:lower()]
if not val then
error('maskToTs: invalid mask \''..v..
'\' at index '..i..' in mask list', 3) end
tval = tval + val
seen[v] = true
end
end
return tostring(tval)
else
error('maskToTs: mask must be a string or table', 3)
end
end
local function objToTs(obj)
if type(obj)=='number' or type(obj)=='string' then
return tostring(obj)
elseif type(obj)=='table' and obj._tsObjectId then
return tostring(obj._tsObjectId)
else
error('objToTs: invalid object \''..tostring(obj)..'\'', 3)
end
end
function bl.raycast(start, stop, mask, ignores)
local startS = vecToTs(start)
local stopS = vecToTs(start)
local maskS = maskToTs(mask)
local ignoresS = {}
for _,v in ipairs(ignores) do
table.insert(ignoresS, objToTs(v))
end
local retS = _bllua_ts.call('containerRaycast', startS, stopS, maskS, unpack(ignoresS))
if retS=='0' then
return nil
else
local hitS, pxS,pyS,pzS, nxS,nyS,nzS = retS:match('^([0-9]+) '..
'(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+) '..
'(%-?[0-9%.e]+) (%-?[0-9%.e]+) (%-?[0-9%.e]+)$')
local hit = toTsObject(hitS)
local pos = vector{tonumber(pxS),tonumber(pyS),tonumber(pzS)}
local norm = vector{tonumber(nxS),tonumber(nyS),tonumber(nzS)}
return hit, pos, norm
end
end
local function tsContainerSearchIterator()
local retS = _bllua_ts.call('containerSearchNext')
if retS=='0' then
return nil
else
return toTsObject(retS)
end
end
function bl.boxSearch(pos, size, mask)
local posS = vecToTs(pos)
local sizeS = vecToTs(size)
local maskS = maskToTs(mask)
_bllua_ts.call('initContainerBoxSearch', posS, sizeS, maskS)
return tsContainerSearchIterator
end
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
local radiusS = tostring(radius)
local maskS = maskToTs(mask)
_bllua_ts.call('initContainerRadiusSearch', posS, radiusS, maskS)
return tsContainerSearchIterator
end
-- Print/Talk/Echo
local function valsToString(vals)
local strs = {}
for i,v in ipairs(vals) do
strs[i] = table.tostring(v)
end
return table.concat(strs, ' ')
end
bl.echo = function(...)
local str = valsToString({...})
_bllua_ts.call('echo', str)
end
print = bl.echo
bl.talk = function(...)
local str = valsToString({...})
_bllua_ts.call('echo', str)
_bllua_ts.call('talk', str)
end
local function createTsObj(keyword, class, name, inherit, props)
local propsT = {}
for k,v in pairs(props) do
if not isValidFuncName(k) then
error('bl.new/datablock: invalid property name \''..k..'\'') end
table.insert(propsT, k..'="'..valToTs(v)..'";')
end
local objS = _bllua_ts.eval(
'return '..keyword..' '..class..'('..
(name or '')..(inherit and (':'..inherit) or '')..'){'..
table.concat(propsT)..'};')
local obj = toTsObject(objS)
if not obj then
error('bl.new/datablock: failed to create object', 3) end
return obj
end
local function parseTsDecl(decl)
local class, name, inherit
if decl:find(' ') then -- class ...
local cl, rest = decl:match('^([^ ]+) ([^ ]+)$')
class = cl
if rest:find(':') then -- class name:inherit
name, inherit = rest:match('^([^:]*):([^:]+)$')
if not name then class = nil end -- error
if name=='' then name = nil end -- class :inherit
else
name = rest
end
else -- class
class = decl
end
if not (
isValidFuncName(class) and
(name==nil or isValidFuncName(name)) and
(inherit==nil or isValidFuncName(inherit)) ) then
error('bl.new/datablock: invalid decl \''..decl..'\'\n'..
'must be of the format: \'className\', \'className name\', '..
'\'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)
return createTsObj('datablock', class, name, inherit, props)
end
setmetatable(bl, tsMeta)
print(' Executed libbl.lua')

52
src/util/libts-support.cs Normal file
View File

@@ -0,0 +1,52 @@
// Read an entire file as text and return its contents as a string
// Used for reading files from zips
function _bllua_ReadEntireFile(%fn) {
%text = "";
%file = new FileObject();
%file.openForRead(%fn);
while (!%file.isEOF()) { %text = %text @ %file.readLine() @ "\r\n"; }
%file.close();
%file.delete();
return %text;
}
// Hack to create/set global variables
// since there's no easy way to do this from the DLL directly
function _bllua_set_var(%name, %val) {
%first = strLwr(getSubStr(%name, 0, 1));
%rest = getSubStr(%name, 1, strLen(%name));
switch$(%first) {
case "a": $a[%rest] = %val; return;
case "b": $b[%rest] = %val; return;
case "c": $c[%rest] = %val; return;
case "d": $d[%rest] = %val; return;
case "e": $e[%rest] = %val; return;
case "f": $f[%rest] = %val; return;
case "g": $g[%rest] = %val; return;
case "h": $h[%rest] = %val; return;
case "i": $i[%rest] = %val; return;
case "j": $j[%rest] = %val; return;
case "k": $k[%rest] = %val; return;
case "l": $l[%rest] = %val; return;
case "m": $m[%rest] = %val; return;
case "n": $n[%rest] = %val; return;
case "o": $o[%rest] = %val; return;
case "p": $p[%rest] = %val; return;
case "q": $q[%rest] = %val; return;
case "r": $r[%rest] = %val; return;
case "s": $s[%rest] = %val; return;
case "t": $t[%rest] = %val; return;
case "u": $u[%rest] = %val; return;
case "v": $v[%rest] = %val; return;
case "w": $w[%rest] = %val; return;
case "x": $x[%rest] = %val; return;
case "y": $y[%rest] = %val; return;
case "z": $z[%rest] = %val; return;
case "_": $_[%rest] = %val; return;
}
error("_bllua_set_var: invalid variable name " @ %name);
return "";
}
echo(" Executed libts-support.cs");

212
src/util/libts.lua Normal file
View File

@@ -0,0 +1,212 @@
-- This Lua code provides some built-in utilities for writing Lua add-ons
-- It is eval'd automatically once BLLua3 has loaded the TS API and environment
-- It only has access to the sandboxed lua environment, just like user code.
ts = _bllua_ts
-- Provide limited OS functions
os = os or {}
function os.time() return math.floor(tonumber(_bllua_ts.call('getSimTime'))/1000) end
function os.clock() return tonumber(_bllua_ts.call('getSimTime'))/1000 end
-- Virtual file class, emulating a file object as returned by io.open
-- Used to wrap io.open to allow reading from zips (using TS)
-- Not perfect because TS file I/O sucks
-- Can't read nulls, can't distinguish between CRLF and LF.
-- Todo someday: actually read the zip in lua?
local file_meta = {
read = function(file, mode)
file:_init()
if not file or type(file)~='table' or not file._is_file then error('File:read: Not a file', 2) end
if file._is_open ~= true then error('File:read: File is closed', 2) end
if mode=='*n' then
local ws, n = file.data:match('^([ \t\r\n]*)([0-9%.%-e]+)', file.pos)
if n then
file.pos = file.pos + #ws + #n
return n
else
return nil
end
elseif mode=='*a' then
local d = file.data:sub(file.pos, #file.data)
file.pos = #file.data + 1
return d
elseif mode=='*l' then
local l, ws = file.data:match('^([^\r\n]*)(\r?\n)', file.pos)
if not l then
l = file.data:match('^([^\r\n]*)$', file.pos); ws = '';
if l=='' then return nil end
end
if l then
file.pos = file.pos + #l + #ws
return l
else
return nil
end
elseif type(mode)=='number' then
local d = file.data:sub(file.pos, file.pos+mode)
file.pos = file.pos + #d
return d
else
error('File:read: Invalid mode \''..mode..'\'', 2)
end
end,
lines = function(file)
file:_init()
return function()
return file:read('*l')
end
end,
close = function(file)
if not file._is_open then error('File:close: File is not open', 2) end
file._is_open = false
end,
__index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end,
_init = function(f)
if not f.data then
f.data = _bllua_ts.call('_bllua_ReadEntireFile', f.filename)
end
end,
}
local function new_file_obj(fn)
local file = {
_is_file = true,
_is_open = true,
pos = 1,
__index = file_meta.__index,
filename = fn,
data = nil,
}
setmetatable(file, file_meta)
return file
end
local function tflip(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end
local allowed_zip_dirs = tflip{
'add-ons', 'base', 'config', 'saves', 'screenshots', 'shaders'
}
local function io_open_absolute(fn, mode)
-- if file exists, use original mode
local res, err = _bllua_io_open(fn, mode)
if res then return res end
-- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader
local dir = fn:match('^[^/]+')
if not allowed_zip_dirs[dir:lower()] then return nil, 'File is not in one of the allowed directories' end
local exist = _bllua_ts.call('isFile', fn) == '1'
if not exist then return nil, err end
if mode~=nil and mode~='r' and mode~='rb' then
return nil, 'Files in zips can only be opened in read mode' end
-- return a temp lua file object with the data
local fi = new_file_obj(fn)
return fi
end
io = io or {}
function io.open(fn, mode, errn)
errn = errn or 1
-- try to open the file with relative path, otherwise use absolute path
local curfn = debug.getfilename(errn + 1) or _bllua_ts.getvar('Con::File')
if curfn == '' then curfn = nil end
if fn:find('^%.') then
local relfn = curfn and fn:find('^%./') and
curfn:gsub('[^/]+$', '')..fn:gsub('^%./', '')
if relfn then
local fi, err = io_open_absolute(relfn, mode, errn+1)
return fi, err, relfn
else
return nil, 'Invalid path', fn
end
else
local fi, err = io_open_absolute(fn, mode, errn+1)
return fi, err, fn
end
end
function io.lines(fn)
local fi, err, fn2 = io.open(fn, nil, 2)
if not fi then error('Error opening file \''..fn2..'\': '..err, 2) end
return fi:lines()
end
function io.type(f)
if type(f)=='table' and f._is_file then
return f._is_open and 'file' or 'closed file'
else
return _bllua_io_type(f)
end
end
-- provide dofile
function dofile(fn, errn)
errn = errn or 1
local fi, err, fn2 = io.open(fn, 'r', errn+1)
if not fi then error('Error executing file \''..fn2..'\': '..err, errn+1) end
print('Executing '..fn2)
local text = fi:read('*a')
fi:close()
return assert(loadstring('--[['..fn2..']]'..text))()
end
-- provide require (just a wrapper for dofile)
-- searches for ?.lua and ?/init.lua in the following directories:
-- location of current file
-- blockland directory
-- current add-on
local function file_exists(fn, errn)
local fi, err, fn2 = io.open(fn, 'r', errn+1)
if fi then
fi:close()
return fn2
else
return nil
end
end
function require(mod)
local fp = mod:gsub('%.', '/')
local fns = {
'./'..fp..'.lua', -- local file
'./'..fp..'/init.lua', -- local library
fp..'.lua', -- global file
fp..'/init.lua', -- global library
}
if fp:lower():find('^add-ons/') then
local addonpath = fp:lower():match('^add-ons/[^/]+')..'/'
table.insert(fns, addonpath..fp..'.lua') -- add-on file
table.insert(fns, addonpath..fp..'/init.lua') -- add-on library
end
for _,fn in ipairs(fns) do
local fne = file_exists(fn, 2)
if fne then
return dofile(fne, 2)
end
end
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
end
function _bllua_smarteval(code)
if (not code:find('^print%(')) and isValidCode('print('..code..')') then
code = 'print('..code..')' end
local f,e = loadstring(code)
if f then
return f()
else
print(e)
end
end
_bllua_ts.call('echo', ' Executed libts.lua')

412
src/util/loadaddons.cs Normal file
View File

@@ -0,0 +1,412 @@
// Package to allow add-ons to use server.lua or client.lua
// instead of or in addition to server.cs or client.cs
// Relevant .lua files are is executed before .cs files.
function _bllua_strEndsWith(%str, %sch) {
%schL = strLen(%sch);
return getSubStr(%str, strLen(%str)-%schL, %schL) $= %sch;
}
//function _bllua_strRemoveEnd(%str, %sch) {
// %schL = strLen(%sch);
// return getSubStr(%str, 0, strLen(%str)-%schL);
//}
function _bllua_fileIsExecCs(%fn) {
return
_bllua_strEndsWith(%fn, "/server.cs" ) ||
_bllua_strEndsWith(%fn, "/server.lua") ||
_bllua_strEndsWith(%fn, "/client.cs" ) ||
_bllua_strEndsWith(%fn, "/client.lua");
}
function _bllua_execAddon(%dirName, %type) {
%i = 0;
%fnLua = "Add-Ons/" @ %dirName @ "/" @ %type @ ".lua";
if(isFile(%fnLua)) { luaexec(%fnLua); %i++; }
%fnCs = "Add-Ons/" @ %dirName @ "/" @ %type @ ".cs";
if(isFile(%fnCs )) { exec(%fnCs ); %i++; }
if(%i==0) {
error("Error Loading Add-On " @ %dirName @ ": Neither " @
%type @ ".cs nor " @ %type @ ".lua exist");
}
}
// Rewrite built-in functions that scan for server.cs or client.cs
// and make them scan for server.lua or client.lua as well
// Note: I had to completely override several large functions,
// many of which are highly redundant, because Badspot didn't know
// what functional decomposition was when he wrote this shit.
package _bllua_addon_exec {
function CustomGameGuiServer::populateAddOnList() {
deleteVariables("$CustomGameGuiServer::AddOn*");
$CustomGameGuiServer::AddOnCount = 0;
%pattern = "Add-Ons/*/server.*";
%filename = findFirstFile(%pattern);
while(isFile(%filename)) {
if(_bllua_fileIsExecCs(%filename)) {
%path = filePath(%filename);
%dirName = getSubStr(%path, strlen("Add-Ons/"), strlen(%path) - strlen("Add-Ons/"));
if(!%seenDirName[%dirName]) {
%seenDirName[%dirName] = 1;
%varName = getSafeVariableName(%dirName);
if(isValidAddOn(%dirName, 1)) {
$CustomGameGuiServer::AddOn[$CustomGameGuiServer::AddOnCount] = %dirName;
$CustomGameGuiServer::AddOnCount++;
}
}
}
%filename = findNextFile(%pattern);
}
}
function GameModeGuiServer::GetMissingAddOns(%filename) {
if(!isFile(%filename)) {
error("ERROR: GameModeGuiServer::GetMissingAddOns(" @ %filename @ ") - file does not exist");
return 0;
}
%path = filePath(%filename);
%missingAddons = "";
%descriptionFile = %path @ "/description.txt";
%previewFile = %path @ "/preview.jpg";
%thumbFile = %path @ "/thumb.jpg";
%saveFile = %path @ "/save.bls";
%colorSetFile = %path @ "/colorSet.txt";
if(!isFile(%descriptionFile))
%missingAddons = %missingAddons TAB %descriptionFile;
if(!isFile(%previewFile))
%missingAddons = %missingAddons TAB %previewFile;
if(!isFile(%thumbFile))
%missingAddons = %missingAddons TAB %thumbFile;
if(!isFile(%saveFile))
%missingAddons = %missingAddons TAB %saveFile;
if(!isFile(%colorSetFile))
%missingAddons = %missingAddons TAB %colorSetFile;
%file = new FileObject(""){};
%file.openForRead(%filename);
while(!%file.isEOF()) {
%line = %file.readLine();
%label = getWord(%line, 0);
%value = trim(getWords(%line, 1, 999));
if(%label !$= "") {
if(getSubStr(%label, 0, 2) !$= "//") {
if(%label $= "ADDON") {
if(!isFile("Add-Ons/" @ %value @ "/description.txt") ||
(!isFile("Add-Ons/" @ %value @ "/server.cs" ) &&
!isFile("Add-Ons/" @ %value @ "/server.lua") ) ) {
if(strlen(%missingAddons) > 0)
%missingAddons = %missingAddons TAB %value;
else
%missingAddons = %value;
}
} else {
if(%label $= "MUSIC") {
if(!isFile("Add-Ons/Music/" @ %value @ ".ogg")) {
if(strlen(%missingAddons) > 0)
%missingAddons = %missingAddons TAB %value @ ".ogg";
else
%missingAddons = %value;
}
}
}
}
}
}
%file.close();
%file.delete();
return %missingAddons;
}
function loadAddOns() {
echo("");
updateAddOnList();
echo("--------- Loading Add-Ons (+BlockLua) ---------");
deleteVariables("$AddOnLoaded__*");
%dir = "Add-Ons/*/server.*";
%filename = findFirstFile(%dir);
%dirCount = 0;
if(isFile("Add-Ons/System_ReturnToBlockland/server.cs")) {
%dirNameList[%dirCount] = "System_ReturnToBlockland";
%dirCount++;
}
while(%filename !$= "") {
if(_bllua_fileIsExecCs(%filename)) {
%path = filePath(%filename);
%dirName = getSubStr(%path, strlen("Add-Ons/"), strlen(%path) - strlen("Add-Ons/"));
if(!%seenDirName[%dirName]) {
%seenDirName[%dirName] = 1;
if(%dirName !$= "System_ReturnToBlockland") {
%dirNameList[%dirCount] = %dirName;
%dirCount++;
}
}
}
%filename = findNextFile(%dir);
}
for(%addOnItr = 0; %addOnItr < %dirCount; %addOnItr++) {
%dirName = %dirNameList[%addOnItr];
%varName = getSafeVariableName(%dirName);
if(!$Server::Dedicated) {
if(getRealTime() - $lastProgressBarTime > 200) {
LoadingProgress.setValue(%addOnItr / %dirCount);
$lastProgressBarTime = getRealTime();
Canvas.repaint();
}
}
if($AddOn__[%varName] $= 1 && isValidAddOn(%dirName)) {
if(%dirName $= "JVS_Content" && $AddOn__["Support_LegacyDoors"] $= 1) {
echo(" Skipping JVS_Content in favor of Support_LegacyDoors");
} else if(!$AddOnLoaded__[%varName]) {
$AddOnLoaded__[%varName] = 1;
%zipFile = "Add-Ons/" @ %dirName @ ".zip";
if(isFile(%zipFile)) {
%zipCRC = getFileCRC(%zipFile);
echo("\c5Loading Add-On: " @ %dirName @ " \c2(CRC:" @ %zipCRC @ ")");
} else {
echo("\c5Loading Add-On: " @ %dirName);
}
if(VerifyAddOnScripts(%dirName)==0) {
echo("\c3ADD-ON " @ %dirName @ " CONTAINS SYNTAX ERRORS\n");
} else {
%oldDBCount = DataBlockGroup.getCount();
_bllua_execAddon(%dirName, "server");
%dbDiff = DataBlockGroup.getCount() - %oldDBCount;
echo("\c2" @ %dbDiff @ " datablocks added.");
echo("");
}
}
}
}
echo("");
}
function loadGameModeAddOns() {
echo("");
echo("--------- Loading Add-Ons (Game Mode) (+BlockLua) ---------");
deleteVariables("$AddOnLoaded__*");
for(%i=0; %i<$GameMode::AddOnCount; %i++) {
%dirName = $GameMode::AddOn[%i];
%varName = getSafeVariableName(%dirName);
if(!$Server::Dedicated) {
if(getRealTime() - $lastProgressBarTime > 200) {
LoadingProgress.setValue(%i / $GameMode::AddOnCount);
$lastProgressBarTime = getRealTime();
Canvas.repaint();
}
}
if(!isValidAddOn(%dirName)) {
error("ERROR: Invalid add-on \'" @ %dirName @ "\' specified for game mode \'" @ $GameModeArg @ "\'");
} else {
$AddOnLoaded__[%varName] = 1;
%zipFile = "Add-Ons/" @ %dirName @ ".zip";
if(isFile(%zipFile)) {
%zipCRC = getFileCRC(%zipFile);
echo("\c5Loading Add-On: " @ %dirName @ " \c2(CRC:" @ %zipCRC @ ")");
} else {
echo("\c5Loading Add-On: " @ %dirName);
}
if(VerifyAddOnScripts(%dirName) == 0) {
echo("\c3ADD-ON " @ %dirName @ " CONTAINS SYNTAX ERRORS\n");
} else {
%oldDBCount = DataBlockGroup.getCount();
_bllua_execAddon(%dirName, "server");
%dbDiff = DataBlockGroup.getCount() - %oldDBCount;
echo("\c2" @ %dbDiff @ " datablocks added.");
echo("");
}
}
}
echo("");
}
function loadClientAddOns() {
echo("");
echo("--------- Loading Client Add-Ons (+BlockLua) ---------");
if(isFile("base/server/crapOns_Cache.cs"))
exec("base/server/crapOns_Cache.cs");
%dir = "Add-Ons/*/client.*";
%filename = findFirstFile(%dir);
%dirCount = 0;
if(isFile("Add-Ons/System_ReturnToBlockland/client.cs")) {
%dirNameList[%dirCount] = "System_ReturnToBlockland";
%dirCount++;
}
while(%filename !$= "") {
if(_bllua_fileIsExecCs(%filename)) {
%path = filePath(%filename);
%dirName = getSubStr(%path, strlen("Add-Ons/"), strlen(%path) - strlen("Add-Ons/"));
if(!%seenDirName[%dirName]) {
%seenDirName[%dirName] = 1;
if(%dirName !$= "System_ReturnToBlockland") {
%dirNameList[%dirCount] = %dirName;
%dirCount++;
}
}
}
%filename = findNextFile(%dir);
}
for(%i=0; %i<%dirCount; %i++) {
%dirName = %dirNameList[%i];
%varName = getSafeVariableName(%dirName);
echo("");
echo("Client checking Add-On: " @ %dirName);
if(!clientIsValidAddOn(%dirName, 1)) {
//deleteVariables("$AddOn__" @ %varName); // wtf
} else {
%name = %dirName;
%zipFile = "Add-Ons/" @ %dirName @ ".zip";
if(isFile(%zipFile)) {
%zipCRC = getFileCRC(%zipFile);
echo("\c5Loading Add-On: " @ %dirName @ " \c2(CRC:" @ %zipCRC @ ")");
} else {
echo("\c5Loading Add-On: " @ %dirName);
}
if(ClientVerifyAddOnScripts(%dirName)==0)
echo("\c3ADD-ON " @ %dirName @ " CONTAINS SYNTAX ERRORS\n");
else
_bllua_execAddon(%dirName, "client");
}
}
echo("");
}
function updateAddOnList() {
echo("\n--------- Updating Add-On List (+BlockLua) ---------");
deleteVariables("$AddOn__*");
if(isFile("config/server/ADD_ON_LIST.cs")) {
exec("config/server/ADD_ON_LIST.cs");
} else {
exec("base/server/defaultAddOnList.cs");
}
if(isFile("base/server/crapOns_Cache.cs")) {
exec("base/server/crapOns_Cache.cs");
}
%dir = "Add-Ons/*/server.*";
%fileCount = getFileCount(%dir);
%filename = findFirstFile(%dir);
while(%filename !$= "") {
if(_bllua_fileIsExecCs(%filename)) {
%path = filePath(%filename);
%dirName = getSubStr(%path, strlen("Add-Ons/"), strlen(%path) - strlen("Add-Ons/"));
if(!%seenDirName[%dirName]) {
%seenDirName[%dirName] = 1;
%varName = getSafeVariableName(%dirName);
echo("Checking Add-On " @ %dirName);
if(!isValidAddOn(%dirName, 1)) {
deleteVariables("$AddOn__" @ %varName);
} else {
if (mFloor($AddOn__[%varName]) <= 0)
$AddOn__[%varName] = -1;
else
$AddOn__[%varName] = 1;
}
}
}
%filename = findNextFile(%dir);
}
echo("");
export("$AddOn__*", "config/server/ADD_ON_LIST.cs");
}
};
activatePackage(_bllua_addon_exec);
// Have to make new versions of these because packaging them is blocked FSFR
function forceRequiredAddOn_L(%dirName) {
if(%dirName $= "JVS_Content") {
if($GameModeArg $= "") {
if($AddOn__["Support_LegacyDoors"] == 1 || !isFile("add-ons/JVS_Content/server.cs") || ($AddOn__["Support_LegacyDoors"] != 1 && $AddOn__["JVS_Content"] != 1)) {
%dirName = "Support_LegacyDoors";
}
} else {
%foundJVSContent = 0;
for(%i=0; %i<$GameMode::AddOnCount; %i++) {
if ($GameMode::AddOn[%i] $= "JVS_Content") {
%foundJVSContent = 1;
}
}
if(!%foundJVSContent)
%dirName = "Support_LegacyDoors";
}
}
if(strstr(%dirName, " ") != -1)
%dirName = strreplace(%dirName, " ", "_");
//if(strstr(%dirName, "/") != -1)
// return $Error::AddOn_Nested;
%varName = getSafeVariableName(%dirName);
if($GameModeArg !$= "") {
%foundIt = 0;
for(%i=0; %i<$GameMode::AddOnCount; %i++) {
if ($GameMode::AddOn[%i] $= %dirName) {
%foundIt = 1;
break;
}
}
if(!%foundIt) {
error("ERROR: ForceRequiredAddOn_L(\'" @ %dirName @ "\') - you can\'t force load an add-on that is not included in gamemode.txt");
if (GameWindowExists() && !$Server::Dedicated) {
schedule(11, 0, MessageBoxOK, "Game Mode Error", "Required add-on " @ %dirName @ " should be added to gamemode.txt");
}
if (!isEventPending($disconnectEvent)) {
$disconnectEvent = schedule(10, 0, disconnect);
}
return $Error::AddOn_NotFound;
}
}
if($AddOnLoaded__[%varName] == 1)
return $Error::None;
if($AddOn__[%varName] $= "" && $GameModeArg $= "" || !isValidAddOn(%dirName)) {
error("ERROR: ForceRequiredAddOn() - " @ %dirName @ " is not a valid add-on");
return $Error::AddOn_NotFound;
}
echo(" Loading Add-On " @ %dirName @ "");
_bllua_execAddon(%dirName, "server");
$AddOnLoaded__[%varName] = 1;
if($AddOn__[%varName] $= 1)
return $Error::None;
else
return $Error::AddOn_Disabled;
}
function loadRequiredAddOn_L(%dirName) {
if(%dirName $= "JVS_Content") {
if($GameModeArg $= "") {
if($AddOn__["Support_LegacyDoors"] == 1 || !isFile("add-ons/JVS_Content/server.cs") || ($AddOn__["Support_LegacyDoors"] != 1 && $AddOn__["JVS_Content"] != 1)) {
%dirName = "Support_LegacyDoors";
}
} else {
%foundJVSContent = 0;
for(%i=0; %i<$GameMode::AddOnCount; %i++) {
if ($GameMode::AddOn[%i] $= "JVS_Content") {
%foundJVSContent = 1;
}
}
if(!%foundJVSContent)
%dirName = "Support_LegacyDoors";
}
}
if(strstr(%dirName, " ") != -1)
%dirName = strreplace(%dirName, " ", "_");
//if(strstr(%dirName, "/") != -1)
// return $Error::AddOn_Nested;
%varName = getSafeVariableName(%dirName);
if ($GameModeArg !$= "") {
%foundIt = 0;
for(%i=0; %i<$GameMode::AddOnCount; %i++) {
if ($GameMode::AddOn[%i] $= %dirName) {
%foundIt = 1;
break;
}
}
if(!%foundIt) {
error("ERROR: LoadRequiredAddOn_L(\'" @ %dirName @ "\') - you can\'t force load an add-on that is not included in gamemode.txt");
if (GameWindowExists() && !$Server::Dedicated)
schedule(11, 0, MessageBoxOK, "Game Mode Error", "Required add-on " @ %dirName @ " should be added to gamemode.txt");
if (!isEventPending($disconnectEvent))
$disconnectEvent = schedule(10, 0, disconnect);
return $Error::AddOn_NotFound;
}
}
if($AddOnLoaded__[%varName] == 1)
return $Error::None;
if($AddOn__[%varName] $= 1) {
echo(" Loading Add-On " @ %dirName @ "");
_bllua_execAddon(%dirName, "server");
$AddOnLoaded__[%varName] = 1;
return $Error::None;
} else {
return $Error::AddOn_Disabled;
}
}
echo(" Executed loadaddons.cs");

360
src/util/std.lua Normal file
View File

@@ -0,0 +1,360 @@
-- Basic functionality that should be standard in Lua
-- Table / List
-- Whether a table contains no keys
function table.empty(t)
for _,_ in pairs(t) do return false end
return true
end
-- Apply a function to each key in a table
function table.map(f, ...)
local ts = {...}
local u = {}
for k,_ in pairs(ts[1]) do
local args = {}
for j=1,#ts do args[j] = ts[j][i] end
u[i] = f(unpack(args))
end
return u
end
function table.map_list(f, ...)
local ts = {...}
local u = {}
for i=1,#ts[1] do
local args = {}
for j=1,#ts do args[j] = ts[j][i] end
u[i] = f(unpack(args))
end
return u
end
-- Swap keys/values
function table.swap(t)
local u = {}
for k,v in pairs(t) do u[v] = k end
return u
end
-- Reverse a list
function table.reverse(l)
local m = {}
for i=1,#l do m[#l-i+1] = l[i] end
return m
end
-- Convert i->v to v->true
function table.values(l)
local u = {}
for _,v in ipairs(l) do u[v] = true end
return u
end
-- Make a list of keys
function table.keys(t)
local u = {}
for k,_ in pairs(t) do table.insert(u, k) end
return u
end
-- Whether a table is a list/array (has only monotonic integer keys)
function table.islist(t)
local n = 0
for i,_ in pairs(t) do
if type(i)~='number' or i%1~=0 then return false end
n = n+1
end
return n==#t
end
-- Append contents of other tables to first table
function table.append(t, ...)
local a = {...}
for _,u in ipairs(a) do
for _,v in ipairs(u) do table.insert(t,v) end
end
return t
end
-- Create a new table containing all keys from any number of tables
-- latter tables in the arg list override prior ones
-- overlaps, NOT appends, integer keys
function table.join(...)
local ts = {...}
local w = {}
for _,t in ipairs(ts) do
for k,v in pairs(t) do w[k] = v end
end
return w
end
-- Whether a table contains a certain value in any key
function table.contains(t,s)
for _,v in pairs(t) do
if v==s then return true end
end
return false
end
function table.contains_list(t,s)
for _,v in ipairs(t) do
if v==s then return true end
end
return false
end
-- Copy a table to another table
function table.copy(t)
local u = {}
for k,v in pairs(t) do u[k] = v end
return u
end
function table.copy_list(l)
local m = {}
for i,v in ipairs(l) do m[i] = v end
return m
end
-- Sort a table in a new copy
function table.sortcopy(t, f)
local u = table.copy_list(t)
table.sort(u, f)
return u
end
-- Remove a value from a table
function table.removevalue(t, r)
local rem = {}
for k,v in pairs(t) do
if v==r then table.insert(rem, k) end
end
for _,k in ipairs(rem) do t[k] = nil end
end
function table.removevalue_list(t, r)
for i = #t, 1, -1 do
if t[i]==r then
table.remove(t, i)
end
end
end
-- Export tables into formatted executable strings
local function tabs(tabLevel)
return (' '):rep(tabLevel)
end
local valueToString
local function tableToString(t, tabLevel, seen)
if type(t)~='table' or (getmetatable(t) and getmetatable(t).__tostring) then
return tostring(t)
elseif table.islist(t) then
if #t==0 then
return '{}'
else
local strs = {}
local containsTables = false
for _,v in ipairs(t) do
if type(v)=='table' then containsTables = true end
table.insert(strs, valueToString(v, tabLevel+1, seen)..',')
end
if containsTables or #t>3 then
return '{\n'..tabs(tabLevel+1)
..table.concat(strs, '\n'..tabs(tabLevel+1))
..'\n'..tabs(tabLevel)..'}'
else
return '{ '..table.concat(strs, ' ')..' }'
end
end
else
local containsNonStringKeys = false
for k,v in pairs(t) do
if type(k)~='string' or k:find('[^a-zA-Z0-9_]') then
containsNonStringKeys = true
elseif type(k)=='table' then
error('table.tostring: table contains a table as key, cannot serialize')
end
end
local strs = {}
if containsNonStringKeys then
for k,v in pairs(t) do
table.insert(strs, '\n'..tabs(tabLevel+1)
..'['..valueToString(k, tabLevel+1, seen)..'] = '
..valueToString(v, tabLevel+1, seen)..',')
end
else
for k,v in pairs(t) do
table.insert(strs, '\n'..tabs(tabLevel+1)
..k..' = '..valueToString(v, tabLevel+1, seen)..',')
end
end
return '{'..table.concat(strs)..'\n'..tabs(tabLevel)..'}'
end
end
valueToString = function(v, tabLevel, seen)
local t = type(v)
if t=='table' then
if seen[v] then
return 'nil --[[ already seen: '..tostring(v)..' ]]'
else
seen[v] = true
return tableToString(v, tabLevel, seen)
end
elseif t=='string' then
return '\''..string.escape(v)..'\''
elseif t=='number' or t=='boolean' then
return tostring(v)
else
--error('table.tostring: table contains a '..t..' value, cannot serialize')
return 'nil --[[ cannot serialize '..t..': '..tostring(v)..' ]]'
end
end
function table.tostring(t)
return tableToString(t, 0, {})
end
-- String
-- Split string into table by separator
-- or by chars if no separator given
-- if regex is not true, sep is treated as a regex pattern
function string.split(str, sep, noregex)
if type(str)~='string' then
error('string.split: argument #1: expected string, got '..type(str), 2) end
if sep==nil or sep=='' then
local t = {}
local ns = #str
for x = 1, ns do
table.insert(t, str:sub(x, x))
end
return t
elseif type(sep)=='string' then
local t = {}
if #str>0 then
local first = 1
while true do
local last, newfirst = str:find(sep, first, noregex)
if not last then break end
table.insert(t, str:sub(first, last-1))
first = newfirst+1
end
table.insert(t, str:sub(first, #str))
end
return t
else
error(
'string.split: argument #2: expected string or nil, got '..type(sep), 2)
end
end
-- Split string to a list of char bytes
function string.bytes(s)
local b = {}
for i=1,#s do
local c = s:sub(i,i)
table.insert(b, c:byte())
end
return b
end
-- Trim leading and trailing whitespace
function string.trim(s, ws)
ws = ws or '[ \t\r\t]'
return s:gsub('^'..ws..'+', ''):gsub(ws..'+$', '')..''
end
-- String slicing and searching using [] operator
local str_meta = getmetatable('')
local str_meta_index_old= str_meta.__index
function str_meta.__index(s,k)
if type(k)=='string' then
return str_meta_index_old[k]
elseif type(k)=='number' then
if k<0 then k = #s+k+1 end
return string.sub(s,k,k)
elseif type(k)=='table' then
local a = k[1]<0 and (#s+k[1]+1) or k[1]
local b = k[2]<0 and (#s+k[2]+1) or k[2]
return string.sub(s,a,b)
end
end
-- String iterator
function string.chars(s)
local i = 0
return function()
i = i+1
if i<=#s then return s:sub(i,i)
else return nil end
end
end
-- Escape sequences
local defaultEscapes = {
['\\'] = '\\\\',
['\''] = '\\\'',
['\"'] = '\\\"',
['\t'] = '\\t',
['\r'] = '\\r',
['\n'] = '\\n',
['\0'] = '\\0',
}
function string.escape(s, escapes)
escapes = escapes or defaultEscapes
local t = {}
for i=1,#s do
local c = s:sub(i,i)
table.insert(t, escapes[c] or c)
end
return table.concat(t)
end
local defaultEscapeChar = '\\'
local defaultUnescapes = {
['\\'] = '\\',
['\''] = '\'',
['\"'] = '\"',
['t'] = '\t',
['r'] = '\r',
['n'] = '\n',
['0'] = '\0',
}
function string.unescape(s, escapeChar, unescapes)
escapeChar = escapeChar or defaultEscapeChar
unescapes = unescapes or defaultUnescapes
local t = {}
local inEscape = false
for i=1,#s do
local c = s:sub(i,i)
if inEscape then
table.insert(t, unescapes[c]
or error('string.unescape: invalid escape sequence: \''
..escapeChar..c..'\''))
elseif c==escapeChar then
inEscape = true
else
table.insert(t, c)
end
end
return table.concat(t)
end
-- IO
io = io or {}
-- Read entire file at once, return nil,err if access failed
function io.readfile(filename)
local fi,err = io.open(filename, 'rb')
if not fi then return nil,err end
local s = fi:read("*a")
fi:close()
return s
end
-- Write data to file all at once, return true if success / false,err if failure
function io.writefile(filename, data)
local fi,err = io.open(filename, 'wb')
if not fi then return false,err end
fi:write(data)
fi:close()
return true,nil
end
-- Math
-- Round
function math.round(x)
return math.floor(x+0.5)
end
-- Mod that accounts for floating point inaccuracy
function math.mod(a,b)
local m = a%b
if m==0 or math.abs(m)<1e-15 or math.abs(m-b)<1e-15 then return 0
else return m end
end
-- Clamp value between min and max
function math.clamp(v, n, x)
return math.min(x, math.max(v, n))
end

219
src/util/vector.lua Normal file
View File

@@ -0,0 +1,219 @@
-- Vector math class with operators
local vector_meta
local vector_new
local function vector_check(v, n, name, argn)
if not v.__is_vector then
error('vector '..name..': argument #'..(argn or 1)
..': expected vector, got '..type(v), n+1) end
end
local function vector_checksamelen(v1, v2, name)
vector_check(v1, 3, name, 1)
vector_check(v2, 3, name, 2)
if #v1~=#v2 then
error('vector '..name..': vector lengths do not match (lengths are '
..#v1..' and '..#v2..')', 3) end
return #v1
end
local function vector_checklen(v1, v2, name, len)
vector_check(v1, 3, name, 1)
vector_check(v2, 3, name, 2)
if #v1~=len or #v2~=len then
error('vector '..name..': vector lengths are not '..len..' (lengths are '
..#v1..' and '..#v2..')', 3) end
end
local function vector_opnnn(name, op)
return function(v1, v2)
local len = vector_checksamelen(v1, v2, name)
local v3 = {}
for i = 1, len do
v3[i] = op(v1[i], v2[i])
end
return vector_new(v3)
end
end
local function vector_opnxn(name, op)
return function(v1, v2)
local v1v = type(v1)=='table' and v1.__is_vector
local v2v = type(v2)=='table' and v2.__is_vector
if v1v and v2v then
local len = vector_checksamelen(v1, v2, name)
local v3 = {}
for i = 1, len do
v3[i] = op(v1[i], v2[i])
end
return vector_new(v3)
else
if v2v then v1,v2 = v2,v1 end
local len = #v1
local v3 = {}
for i = 1, len do
v3[i] = op(v1[i], v2)
end
return vector_new(v3)
end
end
end
local function vector_opn0n(name, op)
return function(v1)
--vector_check(v1, 1, name)
local len = #v1
local v2 = {}
for i = 1, len do
v2[i] = op(v1[i])
end
return vector_new(v2)
end
end
local vector_indices = {x = 1, y = 2, z = 3, w = 4, r = 1, g = 2, b = 3, a = 4}
local vector_meta = {
__is_vector = true,
__index = function(t, k)
if tonumber(k) then return rawget(t, k)
elseif vector_indices[k] then return rawget(t, vector_indices[k])
else return getmetatable(t)[k]
end
end,
__newindex = function(t, k, v)
if tonumber(k) then rawset(t, k, v)
elseif vector_indices[k] then rawset(t, vector_indices[k], v)
else return
end
end,
__add = vector_opnnn('add', function(x1, x2) return x1+x2 end),
__sub = vector_opnnn('sub', function(x1, x2) return x1-x2 end),
__mul = vector_opnxn('mul', function(x1, x2) return x1*x2 end),
__div = vector_opnxn('div', function(x1, x2) return x1/x2 end),
__pow = vector_opnxn('pow', function(x1, x2) return x1^x2 end),
__unm = vector_opn0n('inv', function(x1) return -x1 end),
__concat = nil,
--__len = function(v1) return #v1 end,
__len = nil,
__eq = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'equals')
for i = 1, len do
if v1[i]~=v2[i] then return false end
end
return true
end,
__lt = nil,
__le = nil,
__call = nil,
abs = vector_opn0n('abs', math.abs),
length = function(v1)
--vector_check(v1, 2, 'length')
local len = #v1
local l = 0
for i = 1, len do
l = l + v1[i]^2
end
return math.sqrt(l)
end,
normalize = function(v1)
--vector_check(v1, 2, 'normal')
local length = v1:length()
local len = #v1
local v3 = {}
for i = 1, len do
if length==0 then v3[i] = 0
else v3[i] = v1[i]/length end
end
return vector_new(v3)
end,
__tostring = function(v1)
--vector_check(v1, 2, 'tostring')
local st = {}
local len = #v1
for i = 1, len do
table.insert(st, tostring(v1[i]))
end
return '{ '..table.concat(st, ', ')..' }'
end,
unpack = function(v1) return unpack(v1) end,
floor = vector_opn0n('floor', function(x1) return math.floor(x1) end),
ceil = vector_opn0n('ceil' , function(x1) return math.ceil (x1) end),
round = vector_opn0n('round', function(x1) return math.floor(x1+0.5) end),
dot = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'dot')
local x = 0
for i = 1, len do
x = x + v1[i]*v2[i]
end
return x
end,
cross = function(v1, v2)
vector_checklen(v1, v2, 'cross', 3)
return vector_new{
v1[2]*v2[3] - v1[3]*v2[2],
v1[3]*v2[1] - v1[1]*v2[3],
v1[1]*v2[2] - v1[2]*v2[1],
}
end,
rotateByAngleId = function(v1, r)
--vector_check(v1, 2, 'rotate')
if type(r)~='number' or r%1~=0 then
error('vector rotateByAngleId: invalid rotation '..tostring(r), 2) end
r = r%4
local v2
if r==0 then v2 = vector_new{ v1[1], v1[2], v1[3] }
elseif r==1 then v2 = vector_new{ v1[2], -v1[1], v1[3] }
elseif r==2 then v2 = vector_new{ -v1[1], -v1[2], v1[3] }
elseif r==3 then v2 = vector_new{ -v1[2], v1[1], v1[3] }
else error('vector rotateByAngleId: invalid rotation '..r, 2) end
return v2
end,
rotate2d = function(v, r)
--vector_check(v, 2, 'rotate2d')
if type(r)~='number' then
error('vector rotate2d: invalid rotation '..tostring(r), 2) end
local len = math.sqrt(v[1]^2 + v[2]^2)
local ang = math.atan2(v[2], v[1]) + r
local v2 = vector_new{ math.cos(ang)*len, math.sin(ang)*len }
return v2
end,
tsString = function(v)
--vector_check(v, 2, 'tsString')
return table.concat(v, ' ')
end,
distance = function(v1, v2)
local len = vector_checksamelen(v1, v2, 'distance')
local sum = 0
for i=1,len do
sum = sum + (v1[i] - v2[i])^2
end
return math.sqrt(sum)
end,
copy = function(v)
--vector_check(v, 2, 'copy')
return vector_new(v)
end,
}
vector_new = function(vi)
if vi then
if type(vi)=='string' then
local vi2 = {}
for val in vi:gmatch('[0-9%.%-e]+') do
table.insert(vi2, tonumber(val))
end
vi = vi2
elseif type(vi)~='table' then
error('vector: argument #1: expected input table, got '..type(vi), 2)
end
local v = {}
if #vi>0 then
for i = 1, #vi do v[i] = vi[i] end
else
for n, i in pairs(vector_indices) do v[i] = vi[n] end
if #v==0 then
error('vector: argument #1: table contains no values', 2)
end
end
setmetatable(v, vector_meta)
return v
else
error('vector: argument #1: expected input table, got nil', 2)
end
end
vector = vector_new
return vector_new