util.lua 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. R""( do
  2. -- This Lua code provides some built-in utilities for writing Lua add-ons
  3. -- It is eval'd automatically once BLLua3 has loaded the TS API and environment
  4. -- It only has access to the sandboxed lua environment, just like user code.
  5. -- Implement print using ts.echo so it prints to BL console
  6. print = function(...)
  7. local t = {...}; local o = {};
  8. for i = 1, #t do o[i] = tostring(t[i]) end
  9. ts.echo(table.concat(o, " "))
  10. end
  11. -- Provide limited OS functions
  12. os = {
  13. time = function() return math.floor(tonumber(ts.call("getSimTime"))/1000) end,
  14. clock = function() return tonumber(ts.call("getSimTime"))/1000 end,
  15. }
  16. -- schedule: Use TS's schedule function to schedule lua calls
  17. ts._schedule_table = ts._schedule_table or {}
  18. ts._schedule_by_obj = ts._schedule_by_obj or {}
  19. ts._schedule_nextid = ts._schedule_nextid or 1
  20. function schedule(time, func, ...)
  21. -- get a unique schedule ID
  22. local sched_id = ts._schedule_nextid
  23. ts._schedule_nextid = ts._schedule_nextid+1
  24. -- create a callback to call the function when the schedule ends
  25. local fargs = {...}
  26. local callback = function()
  27. func(unpack(fargs))
  28. end
  29. -- put it in the schedule table
  30. ts._schedule_table[sched_id] = { [1] = callback }
  31. -- use ts schedule to schedule the event
  32. local sched_obj = tonumber(ts.call("schedule", time, 0, "luacall", "_ts_schedule_callback", sched_id))
  33. -- save the schedule reference in a table and return the id
  34. ts._schedule_table[sched_id][2] = sched_obj
  35. return sched_id
  36. end
  37. function _ts_schedule_callback(sched_id)
  38. -- get schedule out of schedule table by id
  39. sched_id = tonumber(sched_id)
  40. local sched = ts._schedule_table[sched_id]
  41. assert(sched, "_ts_schedule_callback: no schedule with id "..sched_id)
  42. ts._schedule_table[sched_id] = nil
  43. -- call it
  44. sched[1]()
  45. end
  46. function cancel(sched_id_s)
  47. sched_id = tonumber(sched_id_s)
  48. -- get the schedule by id out of the table
  49. local sched = ts._schedule_table[sched_id]
  50. if not sched then return end
  51. -- cancel it in ts
  52. ts.call("cancel", sched[2])
  53. -- remove from table
  54. ts._schedule_table[sched_id] = nil
  55. end
  56. -- wrap io.open to allow reading from zips
  57. -- Not perfect because TS file I/O sucks - Can't read nulls, can't distinguish between CRLF and LF.
  58. local file_meta = {
  59. read = function(file, mode)
  60. file:_init()
  61. if not file or type(file)~="table" or not file._is_file then error("File:read: Not a file", 2) end
  62. if file._is_open ~= true then error("File:read: File is closed", 2) end
  63. if mode=="*n" then
  64. local ws, n = file.data:match("^([ \t\r\n]*)([0-9%.%-e]+)", file.pos)
  65. if n then
  66. file.pos = file.pos + #ws + #n
  67. return n
  68. else
  69. return nil
  70. end
  71. elseif mode=="*a" then
  72. local d = file.data:sub(file.pos, #file.data)
  73. file.pos = #file.data + 1
  74. return d
  75. elseif mode=="*l" then
  76. local l, ws = file.data:match("^([^\r\n]*)(\r?\n)", file.pos)
  77. if not l then
  78. l = file.data:match("^([^\r\n]*)$", file.pos); ws = "";
  79. if l=="" then return nil end
  80. end
  81. if l then
  82. file.pos = file.pos + #l + #ws
  83. return l
  84. else
  85. return nil
  86. end
  87. elseif type(mode)=="number" then
  88. local d = file.data:sub(file.pos, file.pos+mode)
  89. file.pos = file.pos + #d
  90. return d
  91. else
  92. error("File:read: Invalid mode \""..mode.."\"", 2)
  93. end
  94. end,
  95. lines = function(file)
  96. file:_init()
  97. return function()
  98. return file:read("*l")
  99. end
  100. end,
  101. close = function(file)
  102. if not file._is_open then error("File:close: File is not open", 2) end
  103. file._is_open = false
  104. end,
  105. __index = function(f, k) return rawget(f, k) or getmetatable(f)[k] end,
  106. _init = function(f)
  107. if not f.data then
  108. f.data = ts.call("_bllua3_ReadEntireFile", f.filename)
  109. end
  110. end,
  111. }
  112. local function new_file_obj(fn)
  113. local file = {
  114. _is_file = true,
  115. _is_open = true,
  116. pos = 1,
  117. __index = file_meta.__index,
  118. filename = fn,
  119. data = nil,
  120. }
  121. setmetatable(file, file_meta)
  122. return file
  123. end
  124. local tflip = function(t) local u = {}; for _, n in ipairs(t) do u[n] = true end; return u; end
  125. local bl_allowed_zip_dirs = tflip{"add-ons", "base", "config", "saves", "screenshots"}
  126. local function io_open_absolute(fn, mode)
  127. -- use original mode if possible
  128. local res, err = _bllua_io_open(fn, mode)
  129. if res then return res end
  130. -- otherwise, if TS sees file but Lua doesn't, it must be in a zip, so use TS reader
  131. local dir = fn:match("^[^/]+")
  132. if not bl_allowed_zip_dirs[dir:lower()] then return nil, "File is not in one of the allowed directories" end
  133. local exist = ts.call("isFile", fn) == "1"
  134. if not exist then return nil, err end
  135. if mode~=nil and mode~="r" then return nil, "Files in zips can only be opened in read mode" end
  136. -- return a temp lua file object with the data
  137. local fi = new_file_obj(fn)
  138. return fi
  139. end
  140. io = {
  141. open = function(fn, mode, errn)
  142. errn = errn or 1
  143. -- try to open the file with relative path, otherwise use absolute path
  144. local curfn = debug.getfilename(errn + 1) or ts.get("Con::File")
  145. if curfn == "" then curfn = nil end
  146. if fn:find("^%.") then
  147. local relfn = curfn and fn:find("^%./") and curfn:gsub("[^/]+$", "")..fn:gsub("^%./", "")
  148. if relfn then
  149. local fi, err = io_open_absolute(relfn, mode, errn+1)
  150. return fi, err, relfn
  151. else
  152. return nil, "Invalid path", fn
  153. end
  154. else
  155. local fi, err = io_open_absolute(fn, mode, errn+1)
  156. return fi, err, fn
  157. end
  158. end,
  159. lines = function(fn)
  160. local fi, err, fn2 = io.open(fn, nil, 2)
  161. if not fi then error("Error opening file \""..fn2.."\": "..err, 2) end
  162. return fi:lines()
  163. end,
  164. type = function(f)
  165. if type(f)=="table" and f._is_file then
  166. return f._is_open and "file" or "closed file"
  167. else
  168. return _bllua_io_type(f)
  169. end
  170. end,
  171. }
  172. -- provide dofile and require
  173. function dofile(fn, errn)
  174. errn = errn or 1
  175. local fi, err, fn2 = io.open(fn, "r", errn+1)
  176. if not fi then error("Error executing file \""..fn2.."\": "..err, errn+1) end
  177. print("Executing "..fn2)
  178. local text = fi:read("*a")
  179. fi:close()
  180. return eval("--[["..fn2.."]]"..text)
  181. end
  182. -- provide eval
  183. function eval(code)
  184. return assert(loadstring(code))()
  185. end
  186. local function file_exists(fn, errn)
  187. local fi, err, fn2 = io.open(fn, "r", errn+1)
  188. if fi then
  189. return fn2
  190. else
  191. return nil
  192. end
  193. end
  194. function require(fn)
  195. local fn1 = "./"..(fn:gsub("%.", "/"))..".lua"
  196. local fn2 = "luainc/"..(fn:gsub("%.", "/"))..".lua"
  197. local fn3 = file_exists(fn1, 2) or file_exists(fn2, 2)
  198. if fn3 then
  199. return dofile(fn3, 2)
  200. else
  201. return requiresecure(fn)
  202. end
  203. end
  204. -- get and set globals
  205. function ts.get(name) return ts.call("_bllua_get_var", name) end
  206. function ts.set(name, val) ts.call("_bllua_set_var", name, val) end
  207. function ts.getobj(obj, name) return ts.call("_bllua_get_var_obj", obj, name) end
  208. function ts.setobj(obj, name, val) ts.call("_bllua_set_var_obj", obj, name, val) end
  209. function _bllua_get_var(name) return _G[name] end
  210. function _bllua_set_var(name, val) _G[name] = val end
  211. -- ts.eval and ts.exec
  212. function ts.eval(code) return ts.call("eval", code) end
  213. function ts.exec(file) return ts.call("exec", file) end
  214. -- packages
  215. --ts._package_table = ts._package_table or {}
  216. --function _bllua_call_package(id, ...)
  217. -- id = tonumber(id)
  218. -- local func = ts._package_table[id] or error("no function with id "..id)
  219. -- --local parent =
  220. -- return func(parent, ...)
  221. --end
  222. --function ts.package(pname, fname, func)
  223. -- local arglist = [[%a, %b, %c, %d, %e, %f, %g, %h, %i, %j, %k, %l, %m, %n, %o, %p, %q, %r, %s, %t]]
  224. -- ts.eval [[
  225. -- package ]]..pname..[[ {
  226. -- function ]]..fname..[[ (]]..arglist..[[) {
  227. -- return luacall("_bllua_call_package", ]]..func_id_pre..[[, ]]..arglist..[[);
  228. -- }
  229. -- };
  230. -- ]]
  231. --end
  232. function ts.toBoolean(v) local n = tonumber(v); return n ~= nil and n ~= 0; end
  233. function ts.toNumber(v) return tonumber(v) end
  234. -- Utility for getting the current filename
  235. function debug.getfilename(level)
  236. if type(level) == "number" then level = level+1 end
  237. local info = debug.getinfo(level)
  238. if not info then return nil end
  239. local filename = info.source:match("^%-%-%[%[([^%]]+)%]%]")
  240. return filename
  241. end
  242. -- Called when pcall fails on a ts->lua call, used to print detailed error info
  243. function _bllua_on_error(err)
  244. err = err:match(": (.+)$") or err
  245. local tracelines = {err}
  246. local level = 2
  247. while true do
  248. local info = debug.getinfo(level)
  249. if not info then break end
  250. local filename = debug.getfilename(level) or info.short_src
  251. local funcname = info.name
  252. if funcname=="dofile" then break end
  253. table.insert(tracelines, string.format("%s:%s in function \'%s\'",
  254. filename,
  255. info.currentline==-1 and "" or info.currentline..":",
  256. funcname
  257. ))
  258. level = level+1
  259. end
  260. return table.concat(tracelines, "\n")
  261. end
  262. print(" Lua util loaded")
  263. end )""