forked from your-land-mirror/cmd_eval
just shuffling commits for no reason #6
76
README.md
76
README.md
@ -154,3 +154,79 @@ List keys of the table (useful for exploring data structures, without flooding y
|
|||||||
|
|
||||||
> for v in oir(100) do print((v:get_luaentity() or {}).name) end
|
> for v in oir(100) do print((v:get_luaentity() or {}).name) end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Coroutine support
|
||||||
|
|
||||||
|
You can call `yield(...)` inside the code you're evaluating. The
|
||||||
|
yielded value will be displayed, same as normal returned value, but
|
||||||
|
you will also be able to resume the computation by typing
|
||||||
|
`/eval_resume`:
|
||||||
|
|
||||||
|
```
|
||||||
|
> for i=1,3 do yield(i) end
|
||||||
|
| 1
|
||||||
|
* coroutine suspended, type /eval_resume to continue
|
||||||
|
/eval_resume
|
||||||
|
* resuming...
|
||||||
|
| 2
|
||||||
|
* coroutine suspended, type /eval_resume to continue
|
||||||
|
/eval_resume
|
||||||
|
* resuming...
|
||||||
|
| 3
|
||||||
|
* coroutine suspended, type /eval_resume to continue
|
||||||
|
/eval_resume
|
||||||
|
| Done.
|
||||||
|
/eval_resume
|
||||||
|
* resuming...
|
||||||
|
* Nothing to resume
|
||||||
|
```
|
||||||
|
|
||||||
|
This can allow you for some shortcuts, for example, visiting all players:
|
||||||
|
|
||||||
|
```
|
||||||
|
/eval for p in players do me:move_to(p:get_pos()); yield() end
|
||||||
|
```
|
||||||
|
|
||||||
|
type `/eval_resume` to visit next one.
|
||||||
|
|
||||||
|
Keep in mind that while the coroutine is paused, it's enviroment can
|
||||||
|
change (tables can be modified, some object refs can become invalid etc.)
|
||||||
|
|
||||||
|
### Forspec output/input
|
||||||
|
|
||||||
|
#### `fsdump(value)`
|
||||||
|
|
||||||
|
Evaluate `fsdump(value)` to show the value in a formspec instead of
|
||||||
|
chat window. This will allow you to select and copy the dumped text,
|
||||||
|
and also keep your chat history from getting spammed. You can call
|
||||||
|
this multiple times, inside a loop, etc. The computation will be
|
||||||
|
paused (see "coroutine support").
|
||||||
|
|
||||||
|
If you close the dump window with ESC or `x` button, the computation
|
||||||
|
will remain paused, and can be resumed by typing `/eval_resume`. If
|
||||||
|
you push `resume` formspec button, it will be resumed normally.
|
||||||
|
|
||||||
|
#### `fsinput(label, text)`
|
||||||
|
|
||||||
|
Evaluate t`fsinput()` (arguments are optional) to open a formspec
|
||||||
|
where you can input some text. The text you enter will be passed back
|
||||||
|
to the coroutine as a return value of the `fsinput()` call.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
`/eval core.get_meta(under):set_string('infotext',fsinput())`
|
||||||
|
|
||||||
|
Will open a window that will let you to edit the `infotext` meta field
|
||||||
|
of the node you're pointing at.
|
||||||
|
|
||||||
|
Or, same thing, but fancier, showing existing infotext in a formspec:
|
||||||
|
|
||||||
|
`/eval local m = core.get_meta(under); m:set_string('infotext',fsinput('infotext', m:get_string('infotext')))`
|
||||||
|
|
||||||
|
If you close `fsinput()` formspec without pusing `send` button, the
|
||||||
|
computation _will not_ be resumed.
|
||||||
|
|
||||||
|
You can still resume it by typing `/eval_resume <text>` - the argument
|
||||||
|
text will be passed instead of the context of text area of the formspec.
|
||||||
|
|
||||||
|
|
||||||
|
256
init.lua
256
init.lua
@ -12,6 +12,7 @@ _G[MODNAME] = api
|
|||||||
-- per-player persistent environments
|
-- per-player persistent environments
|
||||||
api.e = {}
|
api.e = {}
|
||||||
|
|
||||||
|
local WAIT_FOR_FORMSPEC = {"WAIT_FOR_FORMSPEC"} -- unique value
|
||||||
|
|
||||||
local function orange_fmt(...)
|
local function orange_fmt(...)
|
||||||
return core.colorize("#FFA91F", string.format(...))
|
return core.colorize("#FFA91F", string.format(...))
|
||||||
@ -82,8 +83,12 @@ local function create_shared_environment(player_name)
|
|||||||
return core.get_player_by_name(n)
|
return core.get_player_by_name(n)
|
||||||
end,
|
end,
|
||||||
__call = function(_t)
|
__call = function(_t)
|
||||||
k, v = next(pl, k)
|
while next(pl, k) do
|
||||||
return v
|
k, v = next(pl, k)
|
||||||
|
if v:is_valid() then
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -106,6 +111,9 @@ dir(t) -- print table key/values (returns nothing)
|
|||||||
keys(t) -- print table keys (returns nothing)
|
keys(t) -- print table keys (returns nothing)
|
||||||
goir(radius) -- return list of objects around you
|
goir(radius) -- return list of objects around you
|
||||||
oir(radius) -- return iterator for 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)
|
core.chat_send_player(player_name, msg)
|
||||||
end,
|
end,
|
||||||
@ -185,6 +193,36 @@ oir(radius) -- return iterator for objects around you
|
|||||||
return function() return nil end
|
return function() return nil end
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
yield = coroutine.yield,
|
||||||
|
fsdump = function(value)
|
||||||
|
local output = 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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
__index = function(_self, key)
|
__index = function(_self, key)
|
||||||
@ -237,6 +275,93 @@ end
|
|||||||
|
|
||||||
local cc = 0 -- count commands just to identify log messages
|
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",
|
core.register_chatcommand("eval",
|
||||||
{
|
{
|
||||||
params = "<code>",
|
params = "<code>",
|
||||||
@ -268,57 +393,86 @@ core.register_chatcommand("eval",
|
|||||||
|
|
||||||
setfenv(func, env)
|
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,
|
-- Creating a coroutine here, instead of using xpcall,
|
||||||
-- allows us to get a clean stack trace up to this call.
|
-- allows us to get a clean stack trace up to this call.
|
||||||
local res = helper(coroutine.resume(coro))
|
local coro = coroutine.create(func)
|
||||||
res = string.gsub(res, "([^\n]+)", "| %1")
|
|
||||||
if ok then
|
last_coro_by_player[player_name] = {
|
||||||
core.log("info", string.format("[cmd_eval][%s] succeeded.", cc))
|
cc = cc,
|
||||||
else
|
coro = coro,
|
||||||
core.log("warning", string.format("[cmd_eval][%s] failed: %s.", cc, dump(res)))
|
env = env,
|
||||||
end
|
}
|
||||||
return ok, res
|
|
||||||
|
return resume_coroutine(player_name)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
core.register_chatcommand("eval_resume",
|
||||||
|
{
|
||||||
|
params = "<string to pass through yield>",
|
||||||
|
description = "Resume previous command",
|
||||||
|
privs = { server = true },
|
||||||
|
func = function(player_name, param)
|
||||||
|
core.log("action", string.format("[cmd_eval][%s] %s resumed previous command", coro_cc, 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_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
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user