local breakpoints = {} local dbg_steps_mode = false local dbg_step_into_func = false local hook_lock = false local current_func local current_func_stack_size local _debug_getinfo = debug.getinfo local _debug_getlocal = debug.getlocal local __pause = debug.pause local __error = error -- 'return' hook not called for some functions -- todo: speedup local function calc_stack_size() local s = debug.traceback("", 2) local count = 0 for i in s:gmatch("\n") do count = count + 1 end return count end debug.sethook(function (e, line) if e == "return" then local info = _debug_getinfo(2) if info.func == current_func then current_func = nil end end if dbg_steps_mode and not hook_lock then hook_lock = true if not dbg_step_into_func then local func = _debug_getinfo(2).func if func ~= current_func then return end if current_func_stack_size ~= calc_stack_size() then return end end current_func = func __pause() debug.pull_events() end hook_lock = false local bps = breakpoints[line] if not bps then return end local source = _debug_getinfo(2).source if not bps[source] then return end current_func = _debug_getinfo(2).func current_func_stack_size = calc_stack_size() __pause("breakpoint") debug.pull_events() end, "lr") local DBG_EVENT_SET_BREAKPOINT = 1 local DBG_EVENT_RM_BREAKPOINT = 2 local DBG_EVENT_STEP = 3 local DBG_EVENT_STEP_INTO_FUNCTION = 4 local DBG_EVENT_RESUME = 5 local DBG_EVENT_GET_VALUE = 6 local __pull_events = debug.__pull_events local __sendvalue = debug.__sendvalue debug.__pull_events = nil debug.__sendvalue = nil function debug.pull_events() local events = __pull_events() if not events then return end for i, event in ipairs(events) do if event[1] == DBG_EVENT_SET_BREAKPOINT then debug.set_breakpoint(event[2], event[3]) elseif event[1] == DBG_EVENT_RM_BREAKPOINT then debug.remove_breakpoint(event[2], event[3]) elseif event[1] == DBG_EVENT_STEP then dbg_steps_mode = true dbg_step_into_func = false elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then dbg_steps_mode = true dbg_step_into_func = true elseif event[1] == DBG_EVENT_RESUME then dbg_steps_mode = false dbg_step_into_func = false elseif event[1] == DBG_EVENT_GET_VALUE then local _, value = _debug_getlocal(event[2] + 3, event[3]) for _, key in ipairs(event[4]) do if value == nil then value = "error: index nil value" break end value = value[key] end __sendvalue(value, event[2], event[3], event[4]) __pause() end end end function debug.set_breakpoint(source, line) local bps = breakpoints[line] if not bps then bps = {} breakpoints[line] = bps end bps[source] = true end function debug.remove_breakpoint(source, line) local bps = breakpoints[line] if not bps then return end bps[source] = nil end function error(message, level) __pause("exception", message) __error(message, level) end -- Lua has no parallelizm, also _set_data does not call any lua functions so -- may be reused one global ffi buffer per lua_State local canvas_ffi_buffer local canvas_ffi_buffer_size = 0 local ipairs_mt_supported = false for i, _ in ipairs(setmetatable({l={1}}, { __ipairs=function(self) return ipairs(self.l) end})) do ipairs_mt_supported = true end if not ipairs_mt_supported then local raw_ipairs = ipairs ipairs = function(t) local metatable = getmetatable(t) if metatable and metatable.__ipairs then return metatable.__ipairs(t) end return raw_ipairs(t) end end function await(co) local res, err while coroutine.status(co) ~= 'dead' do coroutine.yield() res, err = coroutine.resume(co) if err then return res, err end end return res, err end local _ffi = ffi function __vc_Canvas_set_data(self, data) if type(data) == "cdata" then self:_set_data(tostring(_ffi.cast("uintptr_t", data))) end local width = self.width local height = self.height local size = width * height * 4 if size > canvas_ffi_buffer_size then canvas_ffi_buffer = _ffi.new( string.format("unsigned char[%s]", size) ) canvas_ffi_buffer_size = size end for i=0, size - 1 do canvas_ffi_buffer[i] = data[i + 1] end self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer))) end function crc32(bytes, chksum) local chksum = chksum or 0 local length = #bytes if type(bytes) == "table" then local buffer_len = _ffi.new('int[1]', length) local buffer = _ffi.new( string.format("char[%s]", length) ) for i=1, length do buffer[i - 1] = bytes[i] end bytes = _ffi.string(buffer, buffer_len[0]) end return _crc32(bytes, chksum) end -- Check if given table is an array function is_array(x) if #x > 0 then return true end for k, v in pairs(x) do return false end return true end -- Get entry-point and filename from `entry-point:filename` path function parse_path(path) local index = string.find(path, ':') if index == nil then error("invalid path syntax (':' missing)") end return string.sub(path, 1, index-1), string.sub(path, index+1, -1) end function pack.is_installed(packid) return file.isfile(packid..":package.json") end function pack.data_file(packid, name) file.mkdirs("world:data/"..packid) return "world:data/"..packid.."/"..name end function pack.shared_file(packid, name) file.mkdirs("config:"..packid) return "config:"..packid.."/"..name end function timeit(iters, func, ...) local tm = os.clock() for i=1,iters do func(...) end print("[time mcs]", (os.clock()-tm) * 1000000) end ---------------------------------------------- function math.clamp(_in, low, high) return math.min(math.max(_in, low), high) end function math.rand(low, high) return low + (high - low) * math.random() end function math.normalize(num, conf) conf = conf or 1 return (num / conf) % 1 end function math.round(num, places) places = places or 0 local mult = 10 ^ places return math.floor(num * mult + 0.5) / mult end function math.sum(...) local numbers = nil local sum = 0 if type(...) == "table" then numbers = ... else numbers = {...} end for _, v in ipairs(numbers) do sum = sum + v end return sum end ---------------------------------------------- function table.copy(t) local copied = {} for k, v in pairs(t) do copied[k] = v end return copied end function table.deep_copy(t) local copied = {} for k, v in pairs(t) do if type(v) == "table" then copied[k] = table.deep_copy(v) else copied[k] = v end end return setmetatable(copied, getmetatable(t)) end function table.count_pairs(t) local count = 0 for k, v in pairs(t) do count = count + 1 end return count end function table.random(t) return t[math.random(1, #t)] end function table.shuffle(t) for i = #t, 2, -1 do local j = math.random(i) t[i], t[j] = t[j], t[i] end return t end function table.merge(t1, t2) for i, v in pairs(t2) do if type(i) == "number" then t1[#t1 + 1] = v elseif t1[i] == nil then t1[i] = v end end return t1 end function table.map(t, func) for i, v in pairs(t) do t[i] = func(i, v) end return t end function table.filter(t, func) for i = #t, 1, -1 do if not func(i, t[i]) then table.remove(t, i) end end local size = #t for i, v in pairs(t) do local i_type = type(i) if i_type == "number" then if i < 1 or i > size then if not func(i, v) then t[i] = nil end end else if not func(i, v) then t[i] = nil end end end return t end function table.set_default(t, key, default) if t[key] == nil then t[key] = default return default end return t[key] end function table.flat(t) local flat = {} for _, v in pairs(t) do if type(v) == "table" then table.merge(flat, v) else table.insert(flat, v) end end return flat end function table.deep_flat(t) local flat = {} for _, v in pairs(t) do if type(v) == "table" then table.merge(flat, table.deep_flat(v)) else table.insert(flat, v) end end return flat end function table.sub(arr, start, stop) local res = {} start = start or 1 stop = stop or #arr for i = start, stop do table.insert(res, arr[i]) end return res end ---------------------------------------------- local pattern_escape_replacements = { ["("] = "%(", [")"] = "%)", ["."] = "%.", ["%"] = "%%", ["+"] = "%+", ["-"] = "%-", ["*"] = "%*", ["?"] = "%?", ["["] = "%[", ["]"] = "%]", ["^"] = "%^", ["$"] = "%$", ["\0"] = "%z" } function string.pattern_safe(str) return string.gsub(str, ".", pattern_escape_replacements) end local string_sub = string.sub local string_find = string.find local string_len = string.len function string.explode(separator, str, withpattern) if (withpattern == nil) then withpattern = false end local ret = {} local current_pos = 1 for i = 1, string_len(str) do local start_pos, end_pos = string_find( str, separator, current_pos, not withpattern) if (not start_pos) then break end ret[i] = string_sub(str, current_pos, start_pos - 1) current_pos = end_pos + 1 end ret[#ret + 1] = string_sub(str, current_pos) return ret end function string.split(str, delimiter) return string.explode(delimiter, str) end function string.formatted_time(seconds, format) if (not seconds) then seconds = 0 end local hours = math.floor(seconds / 3600) local minutes = math.floor((seconds / 60) % 60) local millisecs = (seconds - math.floor(seconds)) * 1000 seconds = math.floor(seconds % 60) if (format) then return string.format(format, minutes, seconds, millisecs) else return { h = hours, m = minutes, s = seconds, ms = millisecs } end end function string.replace(str, tofind, toreplace) local tbl = string.explode(tofind, str) if (tbl[1]) then return table.concat(tbl, toreplace) end return str end function string.trim(s, char) if char then char = string.pattern_safe(char) else char = "%s" end return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s end function string.trim_right(s, char) if char then char = string.pattern_safe(char) else char = "%s" end return string.match(s, "^(.-)" .. char .. "*$") or s end function string.trim_left(s, char) if char then char = string.pattern_safe(char) else char = "%s" end return string.match(s, "^" .. char .. "*(.+)$") or s end function string.pad(str, size, char) char = char == nil and " " or char local padding = math.floor((size - #str) / 2) local extra_padding = (size - #str) % 2 return string.rep(char, padding) .. str .. string.rep(char, padding + extra_padding) end function string.left_pad(str, size, char) char = char == nil and " " or char local left_padding = size - #str return string.rep(char, left_padding) .. str end function string.right_pad(str, size, char) char = char == nil and " " or char local right_padding = size - #str return str .. string.rep(char, right_padding) end string.lower = utf8.lower string.upper = utf8.upper string.escape = utf8.escape local meta = getmetatable("") function meta:__index(key) local val = string[key] if (val ~= nil) then return val elseif (tonumber(key)) then return string.sub(self, key, key) end end function string.starts_with(str, start) return string.sub(str, 1, string.len(start)) == start end function string.ends_with(str, endStr) return endStr == "" or string.sub(str, -string.len(endStr)) == endStr end function table.has(t, x) for i,v in ipairs(t) do if v == x then return true end end return false end function table.index(t, x) for i,v in ipairs(t) do if v == x then return i end end return -1 end function table.remove_value(t, x) local index = table.index(t, x) if index ~= -1 then table.remove(t, index) end end function table.tostring(t) local s = '[' for i,v in ipairs(t) do s = s..tostring(v) if i < #t then s = s..', ' end end return s..']' end function file.readlines(path) local str = file.read(path) local lines = {} for s in str:gmatch("[^\r\n]+") do table.insert(lines, s) end return lines end function debug.count_frames() local frames = 1 while true do local info = _debug_getinfo(frames) if info then frames = frames + 1 else return frames - 1 end end end function debug.get_traceback(start) local frames = {} local n = 2 + (start or 0) while true do local info = _debug_getinfo(n) if info then table.insert(frames, info) else return frames end n = n + 1 end end package = { loaded = {} } local __cached_scripts = {} local __warnings_hidden = {} function on_deprecated_call(name, alternatives) if __warnings_hidden[name] then return end __warnings_hidden[name] = true events.emit("core:warning", "deprecated call", name, debug.get_traceback(2)) if alternatives then debug.warning("deprecated function called ("..name.."), use ".. alternatives.." instead\n"..debug.traceback()) else debug.warning("deprecated function called ("..name..")\n"..debug.traceback()) end end function reload_module(name) local prefix, name = parse_path(name) local path = prefix..":modules/"..name..".lua" local previous = package.loaded[path] if not previous then debug.log("attempt to reload non-loaded module "..name.." ("..path..")") return end local script, err = load(file.read(path), path) if script == nil then error(err) end local result = script() if not result then return end for i, value in ipairs(result) do previous[i] = value end local copy = table.copy(result) for key, value in pairs(result) do result[key] = nil end for key, value in pairs(copy) do previous[key] = value end end local internal_locked = false -- Load script with caching -- -- path - script path `contentpack:filename`. -- Example `base:scripts/tests.lua` -- -- nocache - ignore cached script, load anyway local function __load_script(path, nocache, env) local packname, filename = parse_path(path) if internal_locked and (packname == "res" or packname == "core") and filename:starts_with("modules/internal") then error("access to core:internal modules outside of [core]") end -- __cached_scripts used in condition because cached result may be nil if not nocache and __cached_scripts[path] ~= nil then return package.loaded[path] end if not file.isfile(path) then error("script '"..filename.."' not found in '"..packname.."'") end local script, err = load(file.read(path), path) if script == nil then error(err) end if env then script = setfenv(script, env) end local result = script() if not nocache then __cached_scripts[path] = script package.loaded[path] = result end return result end function __vc_lock_internal_modules() internal_locked = true end function require(path) if not string.find(path, ':') then local prefix, _ = parse_path(_debug_getinfo(2).source) return require(prefix .. ':' .. path) end local prefix, file = parse_path(path) local env = __vc__pack_envs[prefix] return __load_script(prefix .. ":modules/" .. file .. ".lua", nil, env) end function __scripts_cleanup() debug.log("cleaning scripts cache") for k, v in pairs(__cached_scripts) do local packname, _ = parse_path(k) if packname ~= "core" then debug.log("unloaded "..k) __cached_scripts[k] = nil package.loaded[k] = nil end end end function __vc__error(msg, frame, n, lastn) if events then local frames = debug.get_traceback(1) events.emit( "core:error", msg, table.sub(frames, 1 + (n or 0), lastn and #frames-lastn) ) end return debug.traceback(msg, frame) end function __vc_warning(msg, detail, n) if events then events.emit( "core:warning", msg, detail, debug.get_traceback(1 + (n or 0))) end end function file.name(path) return path:match("([^:/\\]+)$") end function file.stem(path) local name = file.name(path) return name:match("(.+)%.[^%.]+$") or name end function file.ext(path) return path:match("%.([^:/\\]+)$") end function file.prefix(path) return path:match("^([^:]+)") end function file.parent(path) local dir = path:match("(.*)/") if not dir then return file.prefix(path)..":" end return dir end function file.path(path) local pos = path:find(':') return path:sub(pos + 1) end function file.join(a, b) if a[#a] == ':' then return a .. b end return a .. "/" .. b end bit.compile = require "core:bitwise/compiler" bit.execute = require "core:bitwise/executor" function __vc_create_random_methods(random_methods) local index = 1 local buffer = nil local buffer_size = 64 local seed_func = random_methods.seed local random_func = random_methods.random function random_methods:bytes(n) local bytes = Bytearray(n) for i=1,n do bytes[i] = self:random(255) end return bytes end function random_methods:seed(x) seed_func(self, x) buffer = nil end function random_methods:random(a, b) if not buffer or index > #buffer then buffer = random_func(self, buffer_size) index = 1 end local value = buffer[index] if b then value = math.floor(value * (b - a + 1) + a) elseif a then value = math.floor(value * a + 1) end index = index + 4 return value end return random_methods end