just shuffling commits for no reason #6

Merged
whosit merged 11 commits from master into yl_stable 2025-04-05 08:41:12 +02:00
2 changed files with 281 additions and 51 deletions

View File

@ -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
View File

@ -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
)