local MODNAME = core.get_current_modname() local MODPATH = core.get_modpath(MODNAME) local util = dofile(MODPATH .. DIR_DELIM .. "util.lua") local repl_dump = dofile(MODPATH .. DIR_DELIM .. "dump.lua") local api = {} _G[MODNAME] = api -- per-player persistent environments api.e = {} 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:is_player() then pos = me:get_pos() end return pos end, point = function() local me = core.get_player_by_name(player_name) 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, this_obj = function() local me = core.get_player_by_name(player_name) local pointed_thing = util.raycast_crosshair_to_object(me, 200) if pointed_thing then return pointed_thing.ref end return nil end, this_node_pos = function() local me = core.get_player_by_name(player_name) local pointed_thing = util.raycast_crosshair(me, 200, false, false) if pointed_thing then return pointed_thing.under end return vector.round(me:get_pos()) 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( { my_name = player_name, print = function(...) 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, --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 --dump = repl_dump, }, { __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:get_pos() local cmd_env = { 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 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) local coro = coroutine.create(func) local ok local helper = function(...) -- 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 = {...} 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 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 repl_dump(res[2]) else -- combine returned error and stack trace return 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 table.concat(ret_vals, ',\n') end end -- Creating a coroutine here, instead of using xpcall, -- allows us to get a clean stack trace up to this call. local res = helper(coroutine.resume(coro)) res = string.gsub(res, "([^\n]+)", "| %1") if ok then core.log("info", string.format("[cmd_eval][%s] succeeded.", cc)) else core.log("warning", string.format("[cmd_eval][%s] failed: %s.", cc, dump(res))) end return ok, res end } )