local MODNAME = core.get_current_modname() local MODPATH = core.get_modpath(MODNAME) local util = dofile(MODPATH .. DIR_DELIM .. "util.lua") local dump_funcs = dofile(MODPATH .. DIR_DELIM .. "dump.lua") local repl_dump = dump_funcs.repl_dump local dump_dir = dump_funcs.dump_dir local api = {} _G[MODNAME] = api -- per-player persistent environments api.e = {} local WAIT_FOR_FORMSPEC = {"WAIT_FOR_FORMSPEC"} -- unique value local function orange_fmt(...) return core.colorize("#FFA91F", string.format(...)) end local function create_shared_environment(player_name) local magic_keys = { -- These are _functions_ pretending to be _variables_, they will -- be called when indexing global environmet to get their value. me = function() return core.get_player_by_name(player_name) end, my_pos = function() local me = core.get_player_by_name(player_name) local pos = vector.zero() -- FIXME use last command position? if me and me:is_player() then pos = me:get_pos() end return pos end, point = function() local me = core.get_player_by_name(player_name) if me then local pointed_thing = util.raycast_crosshair(me, 200, true, false) if pointed_thing then return pointed_thing.intersection_point end return me:get_pos() end end, this_obj = function() local me = core.get_player_by_name(player_name) if me then local pointed_thing = util.raycast_crosshair_to_object(me, 200) if pointed_thing then return pointed_thing.ref end end return nil end, above = function() local me = core.get_player_by_name(player_name) if me then local pointed_thing = util.raycast_crosshair(me, 200, false, false) if pointed_thing then return pointed_thing.above end end return nil end, under = function() local me = core.get_player_by_name(player_name) if me then local pointed_thing = util.raycast_crosshair(me, 200, false, false) if pointed_thing then return pointed_thing.under end end return nil end, players = function() local pl = core.get_connected_players() local k, v return setmetatable(pl, { __index = function(_t, n) return core.get_player_by_name(n) end, __call = function(_t) while next(pl, k) do k, v = next(pl, k) if v:is_valid() then return v end end end, } ) end, help = function() local msg = [[ # Magic variables: me -- your player object my_pos -- your position here -- your position where command was executed at (does not change) point -- the exact pos you're pointing with the crosshair this_obj -- the obj you're pointing at with the crosshair or nil above -- same as pointed_thing.above of the node you're pointing at or nil under -- same as pointed_thing.under of the node you're pointing at or nil global -- actual global environment (same as _G) players -- use players.name to access a player (supports `for p in players`) # Functions: dir(t) -- print table key/values (returns nothing) keys(t) -- print table keys (returns nothing) goir(radius) -- return list of objects around you oir(radius) -- return iterator for objects around you yield(value) -- yield value and pause eval, resume it with /eval_resume command fsdump(value) -- show the value in a formspec window fsinput(label, text) -- show a form that will return the text you entered ]] core.chat_send_player(player_name, msg) end, } local g = {} -- "do not warn again" flags local global_proxy = setmetatable( {""}, { __index = _G, __newindex = function(_t, k, v) if _G[k] then core.chat_send_player(player_name, orange_fmt("* Overwriting global: %s", dump(k))) else core.chat_send_player(player_name, orange_fmt("* Creating new global: %s", dump(k))) end _G[k] = v end, } ) local eval_env = setmetatable( { --global = _G, -- this works, but dumps whole global env if you just print `cmd_eval` value _G = global_proxy, -- use our proxy to get warnings global = global_proxy, -- just a different name for globals my_name = player_name, print = function(...) -- print to chat, instead of console local msg = '< ' for i = 1, select('#', ...) do if i > 1 then msg = msg .. '\t' end msg = msg .. tostring(select(i, ...)) end core.chat_send_player(player_name, msg) end, dir = function(o) core.chat_send_player(player_name, dump_dir(o)) end, keys = function(o) -- collect all keys of the table, no values if type(o) == "table" then local keys = {} for k, _ in pairs(o) do local key = k local t = type(key) if t == "string" then key = '"' .. key .. '"' elseif t == "number" then key = '[' .. key .. ']' else key = '[' .. tostring(key) .. ']' end table.insert(keys, key) end table.sort(keys) core.chat_send_player(player_name, table.concat(keys, ',\n')) else core.chat_send_player(player_name, string.format("Not a table: %s", dump(o))) end end, --dump = repl_dump, goir = function(radius) local me = core.get_player_by_name(player_name) if me then local objs = core.get_objects_inside_radius(me:get_pos(), radius) return objs else return {} end end, oir = function(radius) local me = core.get_player_by_name(player_name) if me then return core.objects_inside_radius(me:get_pos(), radius) else return function() return nil end end end, yield = coroutine.yield, fsdump = function(value) local output = type(value) == "string" and value or dump(value) local fs = { "formspec_version[6]", "size[19,10]", "textarea[0.1,0.8;18.8,8.1;a;Output;", core.formspec_escape(output), "]", "button_exit[18.1,0.1;0.8,0.7;x;x]", "button_exit[15.9,9.1;3,0.8;resume;resume]", } core.show_formspec(player_name, "cmd_eval:dump", table.concat(fs, "")) coroutine.yield(WAIT_FOR_FORMSPEC) return value end, fsinput = function(label, text) label = label and tostring(label) or "Input" text = text and tostring(text) or "" local fs = { "formspec_version[6]", "size[19,10]", "textarea[0.1,0.8;18.8,8.1;input;", core.formspec_escape(label), ";", core.formspec_escape(text), "]", "button_exit[18.1,0.1;0.8,0.7;x;x]", "button_exit[15.9,9.1;3,0.8;send;send]", } core.show_formspec(player_name, "cmd_eval:input", table.concat(fs, "")) local result = coroutine.yield(WAIT_FOR_FORMSPEC) return result end, ascii = util.ascii, unascii = util.unascii, }, { __index = function(_self, key) -- we give warnings on accessing undeclared var because it's likely a typo local res = rawget(_G, key) if res == nil then local magic = magic_keys[key] if magic then return magic() elseif not g[key] then core.chat_send_player(player_name, orange_fmt("* Accessing undeclared variable: %s", dump(key))) g[key] = true -- warn only once end end return res end -- there's no __newindex method because we allow assigning -- "globals" inside snippets, since those will be only -- accessible to eval and stored in `eval_env` } ) return eval_env end local function create_command_environment(player_name) local shared_env = api.e[player_name] if not shared_env then shared_env = create_shared_environment(player_name) api.e[player_name] = shared_env end local me = core.get_player_by_name(player_name) local here = me and me:get_pos() local cmd_env = { -- This is a special _per-command_ environment. -- The rationale is: each command should have it's own "here" -- It may matter when we start long-running tasks or something. here = here, } setmetatable( cmd_env, { __index = shared_env, __newindex = shared_env, } ) return cmd_env end local cc = 0 -- count commands just to identify log messages local last_coro_by_player = {} -- setmetatable({}, -- { -- __index = function(t, k) local new = {}; -- rawset(t, k, new); -- return new -- end -- } -- ) local helper = function(coro, env, ...) -- We need this helper to access returned values -- twice - to get the number and to make a table with -- them. -- -- This is a little convoluted, but it's to make sure -- that evaluating functions like: -- -- (function() return 1,nil,3 end)() -- -- will display all returned values. local n = select("#", ...) local res = {...} local ok = res[1] if n == 1 then -- In some cases, calling a function can return literal "nothing": -- + Executing loadstring(`x = 1`) returns "nothing". -- + API calls also can sometimes return literal "nothing" instead of nil return ok, ok and "Done." or "Failed without error message." elseif n == 2 then -- returned single value or error env._ = res[2] -- store result in "_" per-user "global" variable if ok then return ok, repl_dump(res[2]), res[2] else -- combine returned error and stack trace return ok, string.format("%s\n%s", res[2], debug.traceback(coro)) end else -- returned multiple values: display one per line env._ = res[2] -- store result in "_" per-user "global" variable local ret_vals = {} for i=2, n do table.insert(ret_vals, repl_dump(res[i])) end return ok, table.concat(ret_vals, ',\n') end end local function resume_coroutine(player_name, ...) local last = last_coro_by_player[player_name] if not last then return false, "* Nothing to resume" end local c_id, coro, env = last.cc, last.coro, last.env local coro_status = coroutine.status(coro) if coro_status ~= "suspended" then return false, "* Cannot resume dead coroutine" end local ok, res, raw = helper(coro, env, coroutine.resume(coro, ...)) res = string.gsub(res, "([^\n]+)", "| %1") coro_status = coroutine.status(coro) if coro_status == "suspended" then if raw == WAIT_FOR_FORMSPEC then res = "* Waiting for formspec..." else res = res .. "\n* coroutine suspended, type /eval_resume to continue" end else -- coroutine is dead, clean it up last_coro_by_player[player_name] = nil end if ok then core.log("info", string.format("[cmd_eval][%s] succeeded.", c_id)) else core.log("warning", string.format("[cmd_eval][%s] failed: %s.", c_id, dump(res))) end return ok, res end core.register_chatcommand("eval", { params = "", description = "Execute and dump value into chat", privs = { server = true }, func = function(player_name, param) if param == "" then return false, "Gib code pls" end cc = cc + 1 local code = param -- echo input back core.chat_send_player(player_name, "> " .. code) core.log("action", string.format("[cmd_eval][%s] %s entered %s.", cc, player_name, dump(param))) local func, err = loadstring('return ' .. code, "code") if not func then func, err = loadstring(code, "code") end if not func then core.log("action", string.format("[cmd_eval][%s] parsing failed.", cc)) return false, err end local env = create_command_environment(player_name) setfenv(func, env) -- Creating a coroutine here, instead of using xpcall, -- allows us to get a clean stack trace up to this call. local coro = coroutine.create(func) last_coro_by_player[player_name] = { cc = cc, coro = coro, env = env, } return resume_coroutine(player_name) end } ) core.register_chatcommand("eval_resume", { params = "", description = "Resume previous command", privs = { server = true }, func = function(player_name, param) core.log("action", string.format("[cmd_eval] %s resumed previous command", player_name, dump(param))) core.chat_send_player(player_name, "* resuming...") -- it's possible to send the string back to the coroutine through this return resume_coroutine(player_name, param or "") end } ) core.register_chatcommand("eval_reset", { description = "Restore your environment by clearing all variables you assigned", privs = { server = true }, func = function(player_name, param) core.log("action", string.format("[cmd_eval] %s reset their environmet", player_name, dump(param))) api.e[player_name] = nil return true, orange_fmt("* Your environment has been reset to default.") end } ) core.register_on_player_receive_fields( function(player, formname, fields) if formname == "cmd_eval:dump" or formname == "cmd_eval:input" then local player_name = player and player.is_player and player:is_player() and player:get_player_name() if not player_name then return end if fields.resume or fields.send then -- check for correct privs again, just in case if not core.check_player_privs(player_name, { server = true }) then return true end -- check if there's anything wating for the formspec local last = last_coro_by_player[player_name] if not last then return true -- nothing to resume end -- Resuming coroutine only in specific cases allows us -- to close the formspec, do something, then resume -- execution by typing `/eval_resume [input]` local ok, res, show_res if formname == "cmd_eval:input" and fields.send then -- Player had fsinput() call pending and pushed -- `send` - pass their input back to the coroutine local input = fields.input or "" ok, res = resume_coroutine(player_name, input) show_res = true elseif formname == "cmd_eval:dump" and fields.resume then -- Player had dump window open, and pushed -- `resume` so we resume the coro. ok, res = resume_coroutine(player_name) show_res = true end if show_res then if ok then core.chat_send_player(player_name, res) else -- not sure if we need to handle errors here in some way? core.chat_send_player(player_name, res) end end end return true end return -- did not match known FS names end )