Compare commits

...

10 Commits

12 changed files with 996 additions and 845 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

3
.gitignore vendored
View File

@@ -1,2 +1 @@
.* build/
!.gitignore

12
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"Lua.diagnostics.globals": [
"_bllua_ts",
"_bllua_requiresecure",
"_bllua_on_unload"
],
"Lua.runtime.version": "Lua 5.1",
"Lua.diagnostics.disable": [
"lowercase-global",
"undefined-global"
]
}

Binary file not shown.

13
compile.bat Normal file
View File

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

52
compiling.md Normal file
View File

@@ -0,0 +1,52 @@
## Compiling on Windows with MSYS2 (32-bit)
Follow these steps to build `BlockLua.dll` for 32-bit Windows using MSYS2's MinGW-w64 i686 toolchain.
### 1) Install MSYS2
- Download and install MSYS2 from `https://www.msys2.org/`.
- After installing, open the "MSYS2 MSYS" terminal once and update the package database if prompted.
### 2) Install required packages (i686 / 32-bit)
Run this in the "MSYS2 MSYS" or "MSYS2 MinGW 32-bit" terminal:
```bash
pacman -Sy --needed mingw-w64-i686-toolchain mingw-w64-i686-binutils mingw-w64-i686-lua51
```
What these packages are for:
- mingw-w64-i686-toolchain: 32-bit C/C++ compiler suite (g++, libstdc++, runtime libs) to build Windows binaries.
- mingw-w64-i686-binutils: Linker and binary utilities (ld, as, objdump) used by the compiler and for optional inspection.
- mingw-w64-i686-lua51: Lua 5.1 development files for 32-bit (import library). We use its import library to link; at runtime you can use the provided `lua5.1.dll`.
### 3) Build (PowerShell, recommended)
- Open PowerShell in the repo root.
- Run the script:
```powershell
compile.bat
```
What the script does:
- Temporarily prepends `C:\\msys64\\mingw32\\bin` to PATH so the 32-bit toolchain is used.
- Compiles the project with `-m32` and links against `lua5.1`.
- Produces `build\BlockLua.dll` and `build\BlockLua-Unsafe.dll`.
### 4) Optional: Verify 32-bit output
If you installed binutils, you can check the architecture:
```powershell
objdump -f build\BlockLua.dll | Select-String i386
```
You should see `architecture: i386` in the output.
### Notes
- Ensure you installed the i686 (32-bit) variants of the packages; x86_64 packages wont work for a 32-bit build.
- If the linker cannot find `-llua5.1`, confirm `mingw-w64-i686-lua51` is installed and you are using the `mingw32` toolchain (not `x86_64`).

View File

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

View File

@@ -9,32 +9,32 @@ _bllua_on_unload = {}
-- Utility for getting the current filename -- Utility for getting the current filename
function debug.getfilename(level) function debug.getfilename(level)
if type(level) == 'number' then level = level+1 end if type(level) == 'number' then level = level + 1 end
local info = debug.getinfo(level) local info = debug.getinfo(level)
if not info then return nil end if not info then return nil end
local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]') local filename = info.source:match('^%-%-%[%[([^%]]+)%]%]')
return filename return filename
end end
-- Called when pcall fails on a ts->lua call, used to print detailed error info -- Called when pcall fails on a ts->lua call, used to print detailed error info
function _bllua_on_error(err) function _bllua_on_error(err)
err = err:match(': (.+)$') or err err = err:match(': (.+)$') or err
local tracelines = {err} local tracelines = { err }
local level = 2 local level = 2
while true do while true do
local info = debug.getinfo(level) local info = debug.getinfo(level)
if not info then break end if not info then break end
local filename = debug.getfilename(level) or info.short_src local filename = debug.getfilename(level) or info.short_src
local funcname = info.name local funcname = info.name
if funcname=='dofile' then break end if funcname == 'dofile' then break end
table.insert(tracelines, string.format('%s:%s in function \'%s\'', table.insert(tracelines, string.format('%s:%s in function \'%s\'',
filename, filename,
info.currentline==-1 and '' or info.currentline..':', info.currentline == -1 and '' or info.currentline .. ':',
funcname funcname
)) ))
level = level+1 level = level + 1
end end
return table.concat(tracelines, '\n') return table.concat(tracelines, '\n')
end end
_bllua_ts.echo(' Executed bllua-env.lua') _bllua_ts.echo(' Executed bllua-env.lua')

View File

@@ -4,129 +4,129 @@
-- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy -- Class hierarchy, adapted from https://notabug.org/Queuenard/blockland-DLL-tools/src/master/class_hierarchy
bl.class('SimObject') bl.class('SimObject')
bl.class('ScriptObject', 'SimObject') bl.class('ScriptObject', 'SimObject')
bl.class('SimSet', 'SimObject') bl.class('SimSet', 'SimObject')
bl.class('SimGroup', 'SimSet') bl.class('SimGroup', 'SimSet')
bl.class('GuiControl', 'SimGroup') bl.class('GuiControl', 'SimGroup')
bl.class('GuiTextCtrl' , 'GuiControl') bl.class('GuiTextCtrl', 'GuiControl')
bl.class('GuiSwatchCtrl' , 'GuiControl') bl.class('GuiSwatchCtrl', 'GuiControl')
bl.class('GuiButtonBaseCtrl' , 'GuiControl') bl.class('GuiButtonBaseCtrl', 'GuiControl')
bl.class('GuiArrayCtrl' , 'GuiControl') bl.class('GuiArrayCtrl', 'GuiControl')
bl.class('GuiScrollCtrl' , 'GuiControl') bl.class('GuiScrollCtrl', 'GuiControl')
bl.class('GuiMouseEventCtrl' , 'GuiControl') bl.class('GuiMouseEventCtrl', 'GuiControl')
bl.class('GuiProgressCtrl' , 'GuiControl') bl.class('GuiProgressCtrl', 'GuiControl')
bl.class('GuiSliderCtrl' , 'GuiControl') bl.class('GuiSliderCtrl', 'GuiControl')
bl.class('GuiConsoleTextCtrl' , 'GuiControl') bl.class('GuiConsoleTextCtrl', 'GuiControl')
bl.class('GuiTSCtrl' , 'GuiControl') bl.class('GuiTSCtrl', 'GuiControl')
bl.class('GuiObjectView', 'GuiTSCtrl') bl.class('GuiObjectView', 'GuiTSCtrl')
bl.class('GameTSCtrl' , 'GuiTSCtrl') bl.class('GameTSCtrl', 'GuiTSCtrl')
bl.class('EditTSCtrl' , 'GuiTSCtrl') bl.class('EditTSCtrl', 'GuiTSCtrl')
bl.class('GuiPlayerView', 'GuiTSCtrl') bl.class('GuiPlayerView', 'GuiTSCtrl')
bl.class('GuiShapeNameHud' , 'GuiControl') bl.class('GuiShapeNameHud', 'GuiControl')
bl.class('GuiHealthBarHud' , 'GuiControl') bl.class('GuiHealthBarHud', 'GuiControl')
bl.class('GuiGraphCtrl' , 'GuiControl') bl.class('GuiGraphCtrl', 'GuiControl')
bl.class('GuiInspector' , 'GuiControl') bl.class('GuiInspector', 'GuiControl')
bl.class('GuiChunkedBitmapCtrl', 'GuiControl') bl.class('GuiChunkedBitmapCtrl', 'GuiControl')
bl.class('GuiInputCtrl' , 'GuiControl') bl.class('GuiInputCtrl', 'GuiControl')
bl.class('GuiNoMouseCtrl' , 'GuiControl') bl.class('GuiNoMouseCtrl', 'GuiControl')
bl.class('GuiBitmapBorderCtrl' , 'GuiControl') bl.class('GuiBitmapBorderCtrl', 'GuiControl')
bl.class('GuiBackgroundCtrl' , 'GuiControl') bl.class('GuiBackgroundCtrl', 'GuiControl')
bl.class('GuiEditorRuler' , 'GuiControl') bl.class('GuiEditorRuler', 'GuiControl')
bl.class('GuiClockHud' , 'GuiControl') bl.class('GuiClockHud', 'GuiControl')
bl.class('GuiEditCtrl' , 'GuiControl') bl.class('GuiEditCtrl', 'GuiControl')
bl.class('GuiFilterCtrl' , 'GuiControl') bl.class('GuiFilterCtrl', 'GuiControl')
bl.class('GuiFrameSetCtrl' , 'GuiControl') bl.class('GuiFrameSetCtrl', 'GuiControl')
bl.class('GuiMenuBar' , 'GuiControl') bl.class('GuiMenuBar', 'GuiControl')
bl.class('GuiMessageVectorCtrl', 'GuiControl') bl.class('GuiMessageVectorCtrl', 'GuiControl')
bl.class('GuiBitmapCtrl' , 'GuiControl') bl.class('GuiBitmapCtrl', 'GuiControl')
bl.class('GuiCrossHairHud', 'GuiBitmapCtrl') bl.class('GuiCrossHairHud', 'GuiBitmapCtrl')
bl.class('ScriptGroup', 'SimGroup') bl.class('ScriptGroup', 'SimGroup')
bl.class('NetConnection', 'SimGroup') bl.class('NetConnection', 'SimGroup')
bl.class('GameConnection', 'NetConnection') bl.class('GameConnection', 'NetConnection')
bl.class('Path', 'SimGroup') bl.class('Path', 'SimGroup')
bl.class('TCPObject', 'SimObject') bl.class('TCPObject', 'SimObject')
bl.class('SOCKObject', 'TCPObject') bl.class('SOCKObject', 'TCPObject')
bl.class('HTTPObject', 'TCPObject') bl.class('HTTPObject', 'TCPObject')
bl.class('SimDataBlock', 'SimObject') bl.class('SimDataBlock', 'SimObject')
bl.class('AudioEnvironment' , 'SimDataBlock') bl.class('AudioEnvironment', 'SimDataBlock')
bl.class('AudioSampleEnvironment', 'SimDataBlock') bl.class('AudioSampleEnvironment', 'SimDataBlock')
bl.class('AudioDescription' , 'SimDataBlock') bl.class('AudioDescription', 'SimDataBlock')
bl.class('GameBaseData' , 'SimDataBlock') bl.class('GameBaseData', 'SimDataBlock')
bl.class('ShapeBaseData' , 'GameBaseData') bl.class('ShapeBaseData', 'GameBaseData')
bl.class('CameraData' , 'ShapeBaseData') bl.class('CameraData', 'ShapeBaseData')
bl.class('ItemData' , 'ShapeBaseData') bl.class('ItemData', 'ShapeBaseData')
bl.class('MissionMarkerData', 'ShapeBaseData') bl.class('MissionMarkerData', 'ShapeBaseData')
bl.class('PathCameraData' , 'ShapeBaseData') bl.class('PathCameraData', 'ShapeBaseData')
bl.class('PlayerData' , 'ShapeBaseData') bl.class('PlayerData', 'ShapeBaseData')
bl.class('StaticShapeData' , 'ShapeBaseData') bl.class('StaticShapeData', 'ShapeBaseData')
bl.class('VehicleData' , 'ShapeBaseData') bl.class('VehicleData', 'ShapeBaseData')
bl.class('FlyingVehicleData' , 'VehicleData') bl.class('FlyingVehicleData', 'VehicleData')
bl.class('WheeledVehicleData', 'VehicleData') bl.class('WheeledVehicleData', 'VehicleData')
bl.class('DebrisData' , 'GameBaseData') bl.class('DebrisData', 'GameBaseData')
bl.class('ProjectileData' , 'GameBaseData') bl.class('ProjectileData', 'GameBaseData')
bl.class('ShapeBaseImageData' , 'GameBaseData') bl.class('ShapeBaseImageData', 'GameBaseData')
bl.class('TriggerData' , 'GameBaseData') bl.class('TriggerData', 'GameBaseData')
bl.class('ExplosionData' , 'GameBaseData') bl.class('ExplosionData', 'GameBaseData')
bl.class('fxLightData' , 'GameBaseData') bl.class('fxLightData', 'GameBaseData')
bl.class('LightningData' , 'GameBaseData') bl.class('LightningData', 'GameBaseData')
bl.class('ParticleEmitterNodeData', 'GameBaseData') bl.class('ParticleEmitterNodeData', 'GameBaseData')
bl.class('SplashData' , 'GameBaseData') bl.class('SplashData', 'GameBaseData')
bl.class('fxDTSBrickData' , 'GameBaseData') bl.class('fxDTSBrickData', 'GameBaseData')
bl.class('ParticleEmitterData' , 'GameBaseData') bl.class('ParticleEmitterData', 'GameBaseData')
bl.class('WheeledVehicleTire' , 'SimDataBlock') bl.class('WheeledVehicleTire', 'SimDataBlock')
bl.class('WheeledVehicleSpring' , 'SimDataBlock') bl.class('WheeledVehicleSpring', 'SimDataBlock')
bl.class('TSShapeConstructor' , 'SimDataBlock') bl.class('TSShapeConstructor', 'SimDataBlock')
bl.class('AudioProfile' , 'SimDataBlock') bl.class('AudioProfile', 'SimDataBlock')
bl.class('ParticleData' , 'SimDataBlock') bl.class('ParticleData', 'SimDataBlock')
bl.class('MaterialPropertyMap', 'SimObject') bl.class('MaterialPropertyMap', 'SimObject')
bl.class('NetObject', 'SimObject') bl.class('NetObject', 'SimObject')
bl.class('SceneObject', 'NetObject') bl.class('SceneObject', 'NetObject')
bl.class('GameBase', 'SceneObject') bl.class('GameBase', 'SceneObject')
bl.class('ShapeBase', 'GameBase') bl.class('ShapeBase', 'GameBase')
bl.class('MissionMarker', 'ShapeBase') bl.class('MissionMarker', 'ShapeBase')
bl.class('SpawnSphere' , 'MissionMarker') bl.class('SpawnSphere', 'MissionMarker')
bl.class('VehicleSpawnMarker', 'MissionMarker') bl.class('VehicleSpawnMarker', 'MissionMarker')
bl.class('Waypoint' , 'MissionMarker') bl.class('Waypoint', 'MissionMarker')
bl.class('StaticShape' , 'ShapeBase') bl.class('StaticShape', 'ShapeBase')
bl.class('ScopeAlwaysShape', 'StaticShape') bl.class('ScopeAlwaysShape', 'StaticShape')
bl.class('Player' , 'ShapeBase') bl.class('Player', 'ShapeBase')
bl.class('AIPlayer', 'Player') bl.class('AIPlayer', 'Player')
bl.class('Camera' , 'ShapeBase') bl.class('Camera', 'ShapeBase')
bl.class('Item' , 'ShapeBase') bl.class('Item', 'ShapeBase')
bl.class('PathCamera' , 'ShapeBase') bl.class('PathCamera', 'ShapeBase')
bl.class('Vehicle' , 'ShapeBase') bl.class('Vehicle', 'ShapeBase')
bl.class('FlyingVehicle' , 'Vehicle') bl.class('FlyingVehicle', 'Vehicle')
bl.class('WheeledVehicle', 'Vehicle') bl.class('WheeledVehicle', 'Vehicle')
bl.class('Explosion' , 'GameBase') bl.class('Explosion', 'GameBase')
bl.class('Splash' , 'GameBase') bl.class('Splash', 'GameBase')
bl.class('Debris' , 'GameBase') bl.class('Debris', 'GameBase')
bl.class('Projectile' , 'GameBase') bl.class('Projectile', 'GameBase')
bl.class('Trigger' , 'GameBase') bl.class('Trigger', 'GameBase')
bl.class('fxLight' , 'GameBase') bl.class('fxLight', 'GameBase')
bl.class('Lightning' , 'GameBase') bl.class('Lightning', 'GameBase')
bl.class('ParticleEmitterNode', 'GameBase') bl.class('ParticleEmitterNode', 'GameBase')
bl.class('ParticleEmitter' , 'GameBase') bl.class('ParticleEmitter', 'GameBase')
bl.class('Precipitation' , 'GameBase') bl.class('Precipitation', 'GameBase')
bl.class('TSStatic' , 'SceneObject') bl.class('TSStatic', 'SceneObject')
bl.class('VehicleBlocker', 'SceneObject') bl.class('VehicleBlocker', 'SceneObject')
bl.class('Marker' , 'SceneObject') bl.class('Marker', 'SceneObject')
bl.class('AudioEmitter' , 'SceneObject') bl.class('AudioEmitter', 'SceneObject')
bl.class('PhysicalZone' , 'SceneObject') bl.class('PhysicalZone', 'SceneObject')
bl.class('fxDayCycle' , 'SceneObject') bl.class('fxDayCycle', 'SceneObject')
bl.class('fxDTSBrick' , 'SceneObject') bl.class('fxDTSBrick', 'SceneObject')
bl.class('fxPlane' , 'SceneObject') bl.class('fxPlane', 'SceneObject')
bl.class('fxSunLight' , 'SceneObject') bl.class('fxSunLight', 'SceneObject')
bl.class('Sky' , 'SceneObject') bl.class('Sky', 'SceneObject')
bl.class('SceneRoot' , 'SceneObject') bl.class('SceneRoot', 'SceneObject')
bl.class('Sun', 'NetObject') bl.class('Sun', 'NetObject')
bl.class('GuiCursor', 'SimObject') bl.class('GuiCursor', 'SimObject')
bl.class('ConsoleLogger' , 'SimObject') bl.class('ConsoleLogger', 'SimObject')
bl.class('QuotaObject' , 'SimObject') bl.class('QuotaObject', 'SimObject')
bl.class('FileObject' , 'SimObject') bl.class('FileObject', 'SimObject')
bl.class('BanList' , 'SimObject') bl.class('BanList', 'SimObject')
bl.class('GuiControlProfile', 'SimObject') bl.class('GuiControlProfile', 'SimObject')
bl.class('MessageVector' , 'SimObject') bl.class('MessageVector', 'SimObject')
bl.class('ActionMap' , 'SimObject') bl.class('ActionMap', 'SimObject')
-- Auto-generated from game scripts -- Auto-generated from game scripts
bl.type('ActionMap::blockBind:1', 'object') bl.type('ActionMap::blockBind:1', 'object')

View File

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

View File

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

View File

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