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
|
||||
```
|
||||
|
||||
### 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
|
||||
api.e = {}
|
||||
|
||||
local WAIT_FOR_FORMSPEC = {"WAIT_FOR_FORMSPEC"} -- unique value
|
||||
|
||||
local function orange_fmt(...)
|
||||
return core.colorize("#FFA91F", string.format(...))
|
||||
@ -82,8 +83,12 @@ local function create_shared_environment(player_name)
|
||||
return core.get_player_by_name(n)
|
||||
end,
|
||||
__call = function(_t)
|
||||
k, v = next(pl, k)
|
||||
return v
|
||||
while next(pl, k) do
|
||||
k, v = next(pl, k)
|
||||
if v:is_valid() then
|
||||
return v
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
)
|
||||
@ -106,6 +111,9 @@ 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,
|
||||
@ -185,6 +193,36 @@ oir(radius) -- return iterator for objects around you
|
||||
return function() return nil 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)
|
||||
@ -237,6 +275,93 @@ 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 = "<code>",
|
||||
@ -268,57 +393,86 @@ core.register_chatcommand("eval",
|
||||
|
||||
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
|
||||
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 = "<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