fsc: secure formspec code.

Implemented for terminal, and signs, rules, inspector and creator interface code.

Also, fix a bazillion usability bugs in here.
This commit is contained in:
Auke Kok 2017-12-27 00:05:17 -08:00
parent 43bebe3a7f
commit 44e0ef1647
17 changed files with 716 additions and 572 deletions

View File

@ -7,6 +7,7 @@ read_globals = {
"dump",
"VoxelManip", "VoxelArea",
"PseudoRandom", "ItemStack",
"SecureRandom",
table = { fields = { "copy" } },
"unpack",
"AreaStore",

View File

@ -1,4 +1,6 @@
local callback = {}
local function register_teleport(name, def)
local function teleport_update_particles(pos, pname)
local tm = math.random(10, 50) / 10
@ -51,17 +53,14 @@ local function register_teleport(name, def)
minetest.register_node(name, def)
end
local context = {}
local function series_progress_reset(player, id)
-- show a formspec to the user where they can explicitly reset
-- series progress, and explain why they might not want to do so
local name = player:get_player_name()
context[name] = id
--FIXME change this to a bunch of labels.
minetest.show_formspec(name, "series:reset",
fsc.show(name,
"size[10,7]" ..
"textlist[0.5,0.5;8.7,4.5;restettext;You have already completed this series.,," ..
"If you want\\, you can reset your progress for this series," ..
@ -70,43 +69,31 @@ local function series_progress_reset(player, id)
"series later\\, and come back regularly to check if that," ..
"has happened.;0;0]" ..
"button[0.6,5.5;4.4,1.5;btn_cancel;I'll come back later]" ..
"button[5.0,5.5;4.4,1.5;btn_reset;Reset progress]"
"button[5.0,5.5;4.4,1.5;btn_reset;Reset progress]",
{id = id},
callback.progress_reset
)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "series:reset" then
return false
end
function callback.progress_reset(player, fields, context)
local name = player:get_player_name()
minetest.close_formspec(name, "")
if not context[name] then
log.fs_data(player, name, formname, fields)
return true
end
if not fields.btn_reset then
context[name] = nil
return true
end
-- reset series progress
local id = context[name]
local pmeta = db.player_get_meta(name)
pmeta.series_progress[id] = nil
pmeta.series_progress[context] = nil
db.player_set_meta(name, pmeta)
minetest.chat_send_player(player:get_player_name(),
"Reset progress for series " .. id)
context[name] = nil
"Reset progress for series " .. context)
return true
end)
end
local function series_enter_choice(player, id)
local function series_enter_choice(player, id, b_sel)
-- Show a formspec allowing the user to choose what box they want to enter
local name = player:get_player_name()
@ -136,47 +123,34 @@ local function series_enter_choice(player, id)
f = f .. ";]"
f = f .. "button[1.4,8.1;3.4,1;enter;Play]"
minetest.show_formspec(name, "series:enter_choice", f)
fsc.show(name, f, {id = id, boxes = boxes, b_sel = b_sel}, callback.enter_choice)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "series:enter_choice" then
return false
end
function callback.enter_choice(player, fields, context)
local name = player:get_player_name()
if not context[name] then
log.fs_data(player, name, formname, fields)
return true
end
if fields.in_series then
local i = tonumber(string.match(fields.in_series, ":(%d+)", 1))
context[name].b_sel = i
series_enter_choice(player, context[name].id)
return true
if i > #context.boxes or i < 1 then
return true
end
series_enter_choice(player, context.id, i)
return
end
if fields.enter then
if not context[name].id or not context[name].b_sel then
if not context.id or not context.b_sel then
return true
end
minetest.close_formspec(name, "")
local i = context[name].b_sel
local id = context[name].id
local i = context.b_sel
local id = context.id
local pmeta = db.player_get_meta(name)
pmeta.series_progress[id] = i
db.player_set_meta(name, pmeta)
boxes.next_series(player, id, true)
context[name] = nil
return true
end
context[name] = nil
return true
end)
end
register_teleport("boxes:enter_teleport", {
description = "Enter teleport",
@ -195,7 +169,6 @@ register_teleport("boxes:enter_teleport", {
return true
end
elseif smeta.meta.type == db.RANDOM_ACCESS_TYPE then
context[player:get_player_name()] = {id = id}
series_enter_choice(player, id)
return true
end
@ -340,21 +313,20 @@ local get_sizes = function(player)
return {20, 25, 30, 35, 40}
end
local do_series_if = function(player)
local do_series_if = function(player, context)
local name = player:get_player_name()
assert(context[name])
assert(context)
local f =
"size[12,9]"
local f = "size[12,9]"
if context[name].exc_sel then
if context.exc_sel then
f = f .. "button[5.6,3;1,1;add;<<]"
end
if context[name].inc_sel then
if context.inc_sel then
f = f .. "button[5.6,4;1,1;remove;>>]"
end
local showall = context[name].showall == "true"
local showall = context.showall == "true"
if showall then
f = f .. "checkbox[6.5,1;showall;Show all boxes;true]"
else
@ -363,19 +335,25 @@ local do_series_if = function(player)
f = f .. "dropdown[0.5,1;5.45;series;"
local series = db.series_get_series()
local s_sel = ""
local s_sel
local i = 0
for k, v in pairs(series) do
i = i + 1
f = f .. "{" .. v.id .. "} " .. minetest.formspec_escape(v.name)
if k ~= #series then
f = f .. ","
end
if context[name].series_id and context[name].series_id == v then
s_sel = k
if context.series_id and context.series_id == v.id then
s_sel = i
end
end
f = f .. ";" .. s_sel .. "]"
if s_sel then
f = f .. ";" .. s_sel .. "]"
else
f = f .. ";]"
end
local series_id = context[name].series_id
local series_id = context.series_id
if series_id then
local smeta = db.series_get_meta(series_id)
@ -390,7 +368,7 @@ local do_series_if = function(player)
f = f .. "textlist[0.5,3;5,6;list_included;"
local boxes = db.series_get_boxes(series_id)
context[name].inc = boxes
context.inc = boxes
for k, box_id in pairs(boxes) do
local bmeta = db.box_get_meta(box_id)
local bname = bmeta.meta.box_name or "[unnamed]"
@ -428,7 +406,7 @@ local do_series_if = function(player)
b_size = b_size + 1
end
end
context[name].exc = b
context.exc = b
for k, box_id in ipairs(b) do
local bmeta = db.box_get_meta(box_id)
if bmeta.meta.status == db.STATUS_SUBMITTED then
@ -447,117 +425,109 @@ local do_series_if = function(player)
f = f .. ";]"
end
minetest.show_formspec(player:get_player_name(), "boxes:series", f)
fsc.show(name, f, context, callback.series)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "boxes:series" then
return false
end
function callback.series(player, fields, context)
local name = player:get_player_name()
if fields.quit then
context[name] = nil
return true
end
-- minimum required permissions
if not minetest.check_player_privs(name, "server") then
log.fs_data(player, name, formname, fields)
context[name] = nil
return true
end
if not context[name] then
log.fs_data(player, name, formname, fields)
context[name] = nil
return true
end
if fields.showall then
context[name].showall = fields.showall
do_series_if(player)
return true
context.showall = fields.showall
do_series_if(player, context)
return
end
if fields.list_included then
local i = tonumber(string.match(fields.list_included, ":(%d+)", 1))
context[name].inc_sel = context[name].inc[i]
do_series_if(player)
return true
context.inc_sel = context.inc[i]
do_series_if(player, context)
return
end
if fields.list_excluded then
local i = tonumber(string.match(fields.list_excluded, ":(%d+)", 1))
context[name].exc_sel = context[name].exc[i]
do_series_if(player)
return true
context.exc_sel = context.exc[i]
do_series_if(player, context)
return
end
if fields.add then
if context[name].exc_sel and context[name].series_id then
db.series_add_at_end(context[name].series_id, context[name].exc_sel)
minetest.log("action", name .. " added box " .. context[name].exc_sel ..
" to series " .. context[name].series_id)
minetest.chat_send_player(name, " Added box " .. context[name].exc_sel ..
" to series " .. context[name].series_id)
context[name].exc_sel = nil
if context.exc_sel and context.series_id then
db.series_add_at_end(context.series_id, context.exc_sel)
minetest.log("action", name .. " added box " .. context.exc_sel ..
" to series " .. context.series_id)
minetest.chat_send_player(name, " Added box " .. context.exc_sel ..
" to series " .. context.series_id)
context.exc_sel = nil
end
do_series_if(player)
return true
do_series_if(player, context)
return
end
if fields.remove then
if context[name].inc_sel and context[name].series_id then
db.series_delete_box(context[name].series_id, context[name].inc_sel)
minetest.log("action", name .. " removed box " .. context[name].inc_sel ..
" from series " .. context[name].series_id)
minetest.chat_send_player(name, " Removed box " .. context[name].inc_sel ..
" from series " .. context[name].series_id)
context[name].inc_sel = nil
if context.inc_sel and context.series_id then
db.series_delete_box(context.series_id, context.inc_sel)
minetest.log("action", name .. " removed box " .. context.inc_sel ..
" from series " .. context.series_id)
minetest.chat_send_player(name, " Removed box " .. context.inc_sel ..
" from series " .. context.series_id)
context.inc_sel = nil
end
do_series_if(player)
return true
do_series_if(player, context)
return
end
-- These three fields are dropdowns or text fields, and sent every time
-- We need to handle every one of them
if fields.series then
context[name].series_id = tonumber(string.match(fields.series, "{(%d+)}", 1))
context.series_id = tonumber(string.match(fields.series, "{(%d+)}", 1))
end
if fields.series_type then
if context[name].series_id then
local smeta = db.series_get_meta(context[name].series_id)
if fields.series_type and not fields.key_enter_field then
if context.series_id then
local smeta = db.series_get_meta(context.series_id)
local ntype = smeta.meta.type
if fields.series_type == "Sequential" then
minetest.chat_send_player(name, "Changing series " .. context.series_id ..
" to type Sequential")
minetest.log("action", name .. " changes series " .. context.series_id ..
" to type Sequential")
ntype = db.SEQUENTIAL_TYPE
elseif fields.series_type == "Random access" then
minetest.chat_send_player(name, "Changing series " .. context.series_id ..
" to type Random")
minetest.log("action", name .. " changes series " .. context.series_id ..
" to type Random")
ntype = db.RANDOM_ACCESS_TYPE
end
smeta.meta.type = ntype
db.series_set_meta(context[name].series_id, smeta)
db.series_set_meta(context.series_id, smeta)
end
end
if fields.series_name then
if context[name].series_id then
local smeta = db.series_get_meta(context[name].series_id)
if fields.series_name and fields.key_enter_field == "series_name" then
if context.series_id then
local smeta = db.series_get_meta(context.series_id)
smeta.name = fields.series_name
db.series_set_meta(context[name].series_id, smeta)
db.series_set_meta(context.series_id, smeta)
minetest.chat_send_player(name, "Renamed series " .. context.series_id ..
" to \"" .. fields.series_name .. "\"")
minetest.log("action", name .. " renamed series " .. context.series_id ..
" to \"" .. fields.series_name .. "\"")
end
end
if fields.series or fields.series_type or fields.series_name then
do_series_if(player)
return true
do_series_if(player, context)
return
end
log.fs_data(player, name, formname, fields)
context[name] = nil
return true
end)
end
do_creator_if = function(player)
do_creator_if = function(player, context)
local name = player:get_player_name()
if not context[name] then
context[name] = {}
context[name].boxes = get_boxes(name)
context[name].sizes = get_sizes(name)
if not context.boxes or not context.sizes then
context.boxes = get_boxes(name)
context.sizes = get_sizes(name)
end
local counts = {
@ -571,7 +541,7 @@ do_creator_if = function(player)
"field_close_on_enter[input;false]" ..
"textlist[0.4,0.5;7.2,7.3;boxes;"
--list of boxes owned, or all in case of admin
for i, v in ipairs(context[name].boxes) do
for i, v in ipairs(context.boxes) do
if i > 1 then f = f .. "," end
local text = ""
if v.status == db.STATUS_SUBMITTED then
@ -602,47 +572,46 @@ do_creator_if = function(player)
if (minetest.check_player_privs(name, "create") and counts.editing + counts.submitted <= limit) or
minetest.check_player_privs(name, "server") then
context[name].cancreate = true
context.cancreate = true
f = f .. "button[8.4,1;3.4,1;new;Create new]"
elseif not context[name].limitreached then
context[name].limitreached = true
elseif not context.limitreached then
context.limitreached = true
minetest.chat_send_player(name, "You have too many unfinished boxes. Box creation will " ..
"become available again once your boxes get accepted.")
end
if context[name].box and context[name].box.status == db.STATUS_EDITING then
if context.box and context.box.status == db.STATUS_EDITING then
f = f .. "button[8.4,3;3.4,1;submit;Submit]"
elseif minetest.check_player_privs(name, "server") and context[name].box and
context[name].box.status == db.STATUS_SUBMITTED then
elseif minetest.check_player_privs(name, "server") and context.box and
context.box.status == db.STATUS_SUBMITTED then
f = f .. "button[8.4,3;3.4,1;reject;Reject]"
f = f .. "button[8.4,4;3.4,1;accept;Accept]"
elseif context[name].box and context[name].box.status == db.STATUS_SUBMITTED then
elseif context.box and context.box.status == db.STATUS_SUBMITTED then
f = f .. "button[8.4,3;3.4,1;retract;Retract]"
end
if not context[name].box or context[name].box.status == db.STATUS_EDITING then
if not context.box or context.box.status == db.STATUS_EDITING then
f = f .. "button[8.4,7;3.4,1;edit;Edit]"
elseif minetest.check_player_privs(name, "server") then
f = f .. "button[8.4,7;3.4,1;edit;Force edit]"
end
if context[name].box then
if context.box then
f = f .. "button[8.4,6;3.4,1;play;Play]"
end
minetest.show_formspec(player:get_player_name(), "boxes:create", f)
fsc.show(name, f, context, callback.boxes_create)
end
local do_creator_size_if = function(player)
local do_creator_size_if = function(player, context)
local name = player:get_player_name()
assert(context[name])
local f =
"size[6.2, 6]" ..
"field_close_on_enter[input;false]" ..
"textlist[0.4,0.5;5.2,4.3;sizes;"
for i, v in ipairs(context[name].sizes) do
for i, v in ipairs(context.sizes) do
if i > 1 then f = f .. "," end
f = f .. tostring(v)
end
@ -650,30 +619,15 @@ local do_creator_size_if = function(player)
"]" ..
"button[0.4,5;5.4,1;create;Choose selected size]"
minetest.show_formspec(player:get_player_name(), "boxes:create", f)
fsc.show(name, f, context, callback.boxes_create)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "boxes:create" then
return false
end
function callback.boxes_create(player, fields, context)
local name = player:get_player_name()
if fields.quit then
return true
end
-- minimum required permissions
if not minetest.check_player_privs(name, "create") and
not minetest.check_player_privs(name, "server") then
log.fs_data(player, name, formname, fields)
context[name] = nil
return true
end
if not context[name] then
log.fs_data(player, name, formname, fields)
return true
end
@ -711,38 +665,32 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if fields.boxes and fields.boxes ~= "INV" then
local s, _ = fields.boxes:gsub(".*:(.*)", "%1")
if not s then
log.fs_data(player, name, formname, fields)
return true
end
local n = tonumber(s, 10)
if n and context[name].boxes[n] then
context[name].box = context[name].boxes[n]
do_creator_if(player)
if n and context.boxes[n] then
context.box = context.boxes[n]
do_creator_if(player, context)
return
else
log.fs_data(player, name, formname, fields)
return true
end
elseif fields.play and context[name].box then
local id = context[name].box.id
elseif fields.play and context.box then
local id = context.box.id
if is_admin or box_get_status(id) == db.STATUS_ACCEPTED or is_builder(id) then
minetest.chat_send_player(name, "Playing box " .. tostring(id))
minetest.registered_chatcommands["enter"].func(name, tostring(id))
else
log.fs_data(player, name, formname, fields)
end
minetest.close_formspec(name, "")
context[name] = nil
elseif fields.edit and context[name].box then
local id = context[name].box.id
return true
elseif fields.edit and context.box then
local id = context.box.id
if is_admin or (box_get_status(id) == db.STATUS_EDITING and is_builder(id)) then
minetest.chat_send_player(name, "Editing box " .. tostring(id))
minetest.registered_chatcommands["edite"].func(name, tostring(id))
else
log.fs_data(player, name, formname, fields)
end
minetest.close_formspec(name, "")
context[name] = nil
elseif fields.submit and context[name].box then
local id = context[name].box.id
return true
elseif fields.submit and context.box then
local id = context.box.id
if is_admin or (box_get_status(id) == db.STATUS_EDITING and is_builder(id)) then
minetest.chat_send_player(name, "Submitting box " .. tostring(id))
minetest.log("action", name .. " submits box " .. tostring(id))
@ -750,71 +698,57 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
irc.say(name .. " has submitted a box for review")
end
box_set_status(id, db.STATUS_SUBMITTED)
else
log.fs_data(player, name, formname, fields)
end
minetest.close_formspec(name, "")
context[name] = nil
elseif fields.accept and context[name].box then
local id = context[name].box.id
return true
elseif fields.accept and context.box then
local id = context.box.id
if is_admin and box_get_status(id) == db.STATUS_SUBMITTED then
minetest.chat_send_player(name, "Accepting box " .. tostring(id))
minetest.log("action", name .. " accepts box " .. tostring(id))
box_set_status(id, db.STATUS_ACCEPTED)
else
log.fs_data(player, name, formname, fields)
end
minetest.close_formspec(name, "")
context[name] = nil
elseif fields.retract and context[name].box then
local id = context[name].box.id
return true
elseif fields.retract and context.box then
local id = context.box.id
if is_builder(id) and box_get_status(id) == db.STATUS_SUBMITTED then
minetest.chat_send_player(name, "Retracted box " .. tostring(id))
minetest.log("action", name .. " retracts box " .. tostring(id))
box_set_status(id, db.STATUS_EDITING)
else
log.fs_data(player, name, formname, fields)
end
minetest.close_formspec(name, "")
context[name] = nil
elseif fields.reject and context[name].box then
local id = context[name].box.id
return true
elseif fields.reject and context.box then
local id = context.box.id
if is_admin and box_get_status(id) == db.STATUS_SUBMITTED then
minetest.chat_send_player(name, "Rejecting box " .. tostring(id))
minetest.log("action", name .. " rejects box " .. tostring(id))
box_set_status(id, db.STATUS_EDITING)
else
log.fs_data(player, name, formname, fields)
end
minetest.close_formspec(name, "")
context[name] = nil
elseif fields.new and context[name].cancreate then
do_creator_size_if(player)
return true
elseif fields.new and context.cancreate then
do_creator_size_if(player, context)
return
elseif fields.series and is_admin then
context[name] = {}
do_series_if(player)
do_series_if(player, {})
return
elseif fields.sizes then
local s, _ = fields.sizes:gsub(".*:(.*)", "%1")
context[name].size = context[name].sizes[tonumber(s, 10)]
elseif fields.create and context[name] and context[name].size and context[name].cancreate then
context.size = context.sizes[tonumber(s, 10)]
return
elseif fields.create and context.size and context.cancreate then
minetest.close_formspec(name, "")
minetest.chat_send_player(name, "Creating a new box with size " .. context[name].size)
minetest.log("action", name .. " creates a new box with size " .. context[name].size)
boxes.make_new(player, context[name].size)
context[name] = nil
else
log.fs_data(player, name, formname, fields)
context[name] = nil
minetest.chat_send_player(name, "Creating a new box with size " .. context.size)
minetest.log("action", name .. " creates a new box with size " .. context.size)
boxes.make_new(player, context.size)
return true
end
return true
end)
end
register_teleport("boxes:creator_teleport", {
description = "Creator teleport",
tiles = {"blocks_tiles.png^[sheet:8x8:2,2"},
on_teleport = function(pos, node, player)
context[player:get_player_name()] = nil
do_creator_if(player)
do_creator_if(player, {})
return true
end,
})

8
mods/fsc/.luacheckrc Normal file
View File

@ -0,0 +1,8 @@
unused_args = false
allow_defined_top = true
read_globals = {
"minetest",
"SecureRandom",
}

1
mods/fsc/description.txt Normal file
View File

@ -0,0 +1 @@
Easier method for creating better and more secure formspecs.

92
mods/fsc/init.lua Normal file
View File

@ -0,0 +1,92 @@
--[[
FormSpec Context ('fsc') mod for minetest
Copyright (C) 2017 Auke Kok <sofar@foo-projects.org>
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
]]--
fsc = {}
local _data = {}
local SRNG = SecureRandom()
assert(SRNG)
local function make_new_random_id()
local s = SRNG:next_bytes(16)
return s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end)
end
function fsc.show(name, formspec, context, callback)
assert(name)
assert(formspec)
assert(callback)
if not context then
context = {}
end
-- erase old context!
local id = "fsc:" .. make_new_random_id()
_data[name] = {
id = id,
name = name,
context = context,
callback = callback,
}
minetest.show_formspec(name, id, formspec)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if not formname:match("fsc:") then
return false
end
local name = player:get_player_name()
local data = _data[name]
if not data then
minetest.log("warning", "fsc: no data for formspec sent by " .. name)
minetest.close_formspec(name, formname)
return
end
if data.id ~= formname then
minetest.log("warning", "fsc: invalid id for formspec sent by " .. name)
minetest.close_formspec(name, formname)
_data[name] = nil
return
end
if data.name ~= name then
minetest.log("error", "fsc: possible hash collision or exploit (name mismatch)")
minetest.close_formspec(name, formname)
_data[name] = nil
return
end
if data then
if data.callback(player, fields, data.context) then
minetest.close_formspec(name, formname)
_data[name] = nil
elseif fields.quit then
_data[name] = nil
end
end
end)
minetest.register_on_leaveplayer(function(player)
_data[player:get_player_name()] = nil
end)

1
mods/fsc/mod.conf Normal file
View File

@ -0,0 +1 @@
name = fsc

123
mods/fsc/readme.md Normal file
View File

@ -0,0 +1,123 @@
## fsc
This mod is designed to help write more secure formspec handling
code. It achieves this by throwing out the concept of "formspec
names" entirely and giving each formspec shown to the player a unique,
random ID. The player can only then submit form data using this unique
ID, and, the handling code can invalidate the ID during processing
automatically.
This reduces the risk that an attacker can forge formspec data and
send uninvited packets to the server. The server will discard any
form data that appears to come from a client that is attempting to
use old or incorrect fsc-created forms and will note this event in
the minetest log.
Because of the simplicity of the approach, mods will no longer need
to focus on basic formspec handling code and can instead spent their
time verifying the proper permissions and input data correctness.
This mod also provides a much more simple way to maintain a formspec
"context" and pass it along to subsequent formspecs. This makes
writing formspec code simpler as the context does not need to be
maintained outside the formspec handling or creation code, and no
memory leakage needs to be worried about.
A player can also only ever obtain one context, and attempting to
use an invalid or outdated context will result in all current valid
formspec contexts being revoked. Combined together, all these features
make formspecs a lot safer to work with.
## Usage
The basic workflow of `fsc` contains of a single function call. Outside
of this function call, there are no other API functions or data.
```
function fsc.show(name, formspec, context, callback)
-- `name`: a playername,
-- `formspec`: a valid formspec string,
-- `context`: any data, may be `nil`
-- `callback`: function(player, fields, context).
```
The return value of `fsc.show()` is always `nil` - it returns nothing.
The callback function will only be called if basic sanity checks on the data
pass requirements. You can implement it simply as follows:
```
local function callback(player, fields, context)
-- `player`: player object,
-- `fields`: table containing formspec data returned by the player client,
-- `context`: any data, will never be `nil`. If no context was passed
-- to `fsc.show()`, it will contain `{}`
return true
end
```
The return value of the callback may be `nil` or `true`. If you return
`nil`, the context is not invalidated, and the player may submit the
formspec using the same ID again. This is useful if the player merely
selects a list item or otherwise performs an action in the form that
does not cause the form to be closed on the client, and you wish to
keep the form open.
If you return `true`, or if you return `nil` and `fields.quit` is set,
then the fsc code will invalidate the ID and close the formspec. You
should return `true` unless you want to keep the form open to the
player.
Making a simple callback handler that shows a new form is therefore
relatively straightforward. The below example passes the current
context data through to the new form. The old form will close, and
the new form will appear to the player with the new content.
```
local function callback(player, fields, context)
local name = player:get_player_name()
if fields.rename then
fsc.show(name,
"field[new_name;What is the new name?;" .. minetest.formspec_escape(context.old_name) .. "]",
context,
callback)
return
else
-- do something else
return true
end
end
```
In some cases, you may wish to show a form without having the
need for a callback, in case the content is just informational and
non-interactive. In that case, you can omit a callback handler by
just inserting an empty callback handler, as follows:
`fsc.show(name, formspec, {}, function() end)`
## Node Formspecs
Node formspecs are not handled. Due to the nature of node/inventory
formspecs, it is inherently impossible to perform the same checks on
node/inventory formspecs as `fsc` can do for (normal) formspecs.
## License
FormSpec Context ('fsc') mod for minetest, licensed under the `ISC` license:
Copyright (C) 2017 Auke Kok <sofar@foo-projects.org>
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -1 +1,2 @@
boxes
fsc

View File

@ -131,7 +131,7 @@ minetest.register_tool("inspector:inspector", {
minetest.formspec_escape(desc).."]"..
"button_exit[2.5,7.5;3,1;close;Close]"
minetest.show_formspec(user:get_player_name(), "inspector:inspector", formspec)
fsc.show(user:get_player_name(), formspec, {}, function() end)
end,
})
@ -152,7 +152,7 @@ minetest.register_chatcommand("inspect", {
minetest.formspec_escape(desc).."]"..
"button_exit[2.5,7.5;3,1;close;Close]"
minetest.show_formspec(name, "inspector:inspector", formspec)
fsc.show(name, formspec, {}, function() end)
return true
end,
})

View File

@ -3,3 +3,4 @@ music
skybox
sfinv
conf
fsc

View File

@ -5,8 +5,6 @@
--]]
local menucontext = {}
local function toggle_music(player)
local m = player:get_attribute("music")
if not m or m == "1" then
@ -41,6 +39,131 @@ sfinv.register_page("menu:lobby", {
end,
})
local function callback_erase(player, fields, context)
local name = player:get_player_name()
if fields.btn_cancel or fields.quit then
return true
end
if not fields.btn_erase or
context ~= tonumber(fields.confirm) or
not boxes.players_editing_boxes[name] then
return true
end
-- wipe the box
local box = boxes.players_editing_boxes[name]
minetest.log("action", name .. " erases all content of box " .. box.box_id)
local minp = vector.add(box.minp, 1)
local maxp = vector.add(box.maxp, -1)
-- wipe nodes with meta separately
for _, p in ipairs(minetest.find_nodes_with_meta(minp, maxp)) do
minetest.remove_node(p)
end
local cid_air = minetest.get_content_id("air")
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid_air
param2[index] = 0
index = index + 1
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
minetest.after(0.1, minetest.fix_light, minp, maxp)
-- reset pedestal number, build time
box.start_edit_time = minetest.get_gametime()
local bmeta = db.box_get_meta(box.box_id)
bmeta.meta.num_items = 0
bmeta.meta.build_time = 0
db.box_set_meta(box.box_id, bmeta)
return true
end
local function callback_landscape(player, fields, context)
local name = player:get_player_name()
if fields.btn_cancel or fields.quit then
return true
end
if not fields.btn_landscape or
context ~= tonumber(fields.confirm) or
not boxes.players_editing_boxes[name] then
return true
end
-- landscape the box
local box = boxes.players_editing_boxes[name]
minetest.log("action", name .. " landscapes box " .. box.box_id)
local minp = vector.add(box.minp, 1)
local maxp = vector.add(box.maxp, -1)
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
local cursor = 8
local stack
for y = minp.y, maxp.y do
local inv = player:get_inventory()
while not stack or stack:is_empty() and cursor > 1 do
stack = inv:get_stack("landscape", cursor)
cursor = cursor - 1
end
if not stack:is_empty() then
local item = stack:take_item()
local iname = item:get_name()
if iname ~= "torches:torch" and minetest.registered_nodes[iname] then
local cid = minetest.get_content_id(iname)
for z = minp.z, maxp.z do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid
param2[index] = 0
index = index + 1
end
end
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
minetest.after(0.1, minetest.fix_light, minp, maxp)
-- reset pedestal number, build time
box.start_edit_time = minetest.get_gametime()
local bmeta = db.box_get_meta(box.box_id)
bmeta.meta.num_items = 0
bmeta.meta.build_time = 0
db.box_set_meta(box.box_id, bmeta)
return true
end
sfinv.register_page("menu:edit", {
title = "Game",
is_in_nav = function(self, player, context)
@ -107,8 +230,7 @@ sfinv.register_page("menu:edit", {
if fields.erase then
local name = player:get_player_name()
local token = math.random(10000, 99999)
menucontext[name] = {token = token}
minetest.show_formspec(player:get_player_name(), "menu:erase",
fsc.show(name,
"size[10,7]" ..
"textlist[0.5,0.5;8.7,2.0;restettext;" ..
"Erasing the content is irreversible\\, and can not," ..
@ -117,16 +239,17 @@ sfinv.register_page("menu:edit", {
"text box\\,and press \"Erase Everything\".;0;0]" ..
"field[4.0,3.5;3.0,1.5;confirm;;]" ..
"button[0.6,5.5;4.4,1.5;btn_cancel;Cancel]" ..
"button[5.0,5.5;4.4,1.5;btn_erase;Erase everything]"
"button[5.0,5.5;4.4,1.5;btn_erase;Erase everything]",
token,
callback_erase
)
end
if fields.landscape then
local name = player:get_player_name()
local token = math.random(10000, 99999)
menucontext[name] = {token = token}
local inv = minetest.get_inventory({type="player", name = name})
inv:set_size("landscape", 10)
minetest.show_formspec(player:get_player_name(), "menu:landscape",
fsc.show(name,
"size[10,10]" ..
"list[current_player:landscape;landscape;0.2,0;1,8;]" ..
"list[current_player;main;1.6,0;8,1;]" ..
@ -139,7 +262,9 @@ sfinv.register_page("menu:edit", {
"label[1.6,7.6;Put the code \\\"" .. token .. "\\\" in the box here:]" ..
"field[6.4,7.4;3.0,1.5;confirm;;]" ..
"button[1.4,8.8;4,1.5;btn_cancel;Cancel]" ..
"button[5.4,8.8;4,1.5;btn_landscape;Landscape this box]"
"button[5.4,8.8;4,1.5;btn_landscape;Landscape this box]",
token,
callback_landscape
)
end
if fields.music then
@ -148,155 +273,6 @@ sfinv.register_page("menu:edit", {
end,
})
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "menu:erase" then
return false
end
local name = player:get_player_name()
if fields.btn_cancel or fields.quit then
menucontext[name] = nil
minetest.close_formspec(player:get_player_name(), "")
return true
end
if not fields.btn_erase or
not menucontext[name] or
not menucontext[name].token or
menucontext[name].token ~= tonumber(fields.confirm) or
not boxes.players_editing_boxes[name] then
--FIXME log formspec data?
menucontext[name] = nil
minetest.close_formspec(player:get_player_name(), "")
return true
end
-- wipe the box
local box = boxes.players_editing_boxes[name]
minetest.log("action", name .. " erases all content of box " .. box.box_id)
local minp = vector.add(box.minp, 1)
local maxp = vector.add(box.maxp, -1)
-- wipe nodes with meta separately
for _, p in ipairs(minetest.find_nodes_with_meta(minp, maxp)) do
minetest.remove_node(p)
end
local cid_air = minetest.get_content_id("air")
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid_air
param2[index] = 0
index = index + 1
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
minetest.after(0.1, minetest.fix_light, minp, maxp)
-- reset pedestal number, build time
box.start_edit_time = minetest.get_gametime()
local bmeta = db.box_get_meta(box.box_id)
bmeta.meta.num_items = 0
bmeta.meta.build_time = 0
db.box_set_meta(box.box_id, bmeta)
menucontext[name] = nil
minetest.close_formspec(player:get_player_name(), "")
return true
end)
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "menu:landscape" then
return false
end
local name = player:get_player_name()
if fields.btn_cancel or fields.quit then
menucontext[name] = nil
minetest.close_formspec(player:get_player_name(), "")
return true
end
if not fields.btn_landscape or
not menucontext[name] or
not menucontext[name].token or
menucontext[name].token ~= tonumber(fields.confirm) or
not boxes.players_editing_boxes[name] then
--FIXME log formspec data?
menucontext[name] = nil
minetest.close_formspec(player:get_player_name(), "")
return true
end
-- landscape the box
local box = boxes.players_editing_boxes[name]
minetest.log("action", name .. " landscapes box " .. box.box_id)
local minp = vector.add(box.minp, 1)
local maxp = vector.add(box.maxp, -1)
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
local cursor = 8
local stack
for y = minp.y, maxp.y do
local inv = player:get_inventory()
while not stack or stack:is_empty() and cursor > 1 do
stack = inv:get_stack("landscape", cursor)
cursor = cursor - 1
end
if not stack:is_empty() then
local item = stack:take_item()
local iname = item:get_name()
if iname ~= "torches:torch" and minetest.registered_nodes[iname] then
local cid = minetest.get_content_id(iname)
for z = minp.z, maxp.z do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid
param2[index] = 0
index = index + 1
end
end
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
minetest.after(0.1, minetest.fix_light, minp, maxp)
-- reset pedestal number, build time
box.start_edit_time = minetest.get_gametime()
local bmeta = db.box_get_meta(box.box_id)
bmeta.meta.num_items = 0
bmeta.meta.build_time = 0
db.box_set_meta(box.box_id, bmeta)
menucontext[name] = nil
minetest.close_formspec(player:get_player_name(), "")
return true
end)
sfinv.register_page("menu:play", {
title = "Game",

1
mods/rules/depends.txt Normal file
View File

@ -0,0 +1 @@
fsc

View File

@ -246,7 +246,7 @@ function rules.show(name, t)
"]" ..
"button_exit[0.7,7;11.2,1;close;Close]"
minetest.show_formspec(name, "rules:", f)
fsc.show(name, f, {}, function() end)
end
minetest.register_chatcommand("rules", {

View File

@ -1,3 +1,4 @@
boxes
log
ranks?
fsc

View File

@ -1,8 +1,6 @@
signs = {}
local context = {}
local function clean_sign_entities(pos)
for _, obj in pairs(minetest.get_objects_inside_radius(pos, 0.5)) do
if not obj:is_player() and obj:get_luaentity().name == "signs:sign" then
@ -113,6 +111,64 @@ local function sign_refresh(pos)
return (count > 0)
end
local function text_callback(player, fields, context)
local name = player:get_player_name()
--validate user was allowed to edit signs
if not boxes.can_edit(player) then
log.fs_data(player, name, "signs:text", fields)
return true
end
if not fields.text then
if fields.quit then
return true
end
fields.text = ""
end
-- validate length of sign text does not exceed max
if string.len(fields.text) > 1000 then
fields.text = fields.text:sub(1, 1000)
end
-- check: validate composed texture string does not exceed max
-- max 1000 characters, each character can take up to 17 chars to encode
-- in tex. So the total size is 17000 + "[combine", which easily fits
-- 64k max texture string.
--validate sign pos is actually within the box that player is in
local pos = context
if not boxes.validate_pos(player, pos) then
return true
end
-- verify node is still a sign
local node = minetest.get_node(pos)
if node.name ~= "signs:sign" and node.name ~= "signs:sign_wall" then
-- sign no longer exists. Could be lag.
return true
end
-- erase sign
clean_sign_entities(pos)
local meta = minetest.get_meta(pos)
meta:set_string("text", fields.text)
meta:mark_as_private("text")
-- for lobby signs that need a refresh
if minetest.get_node_timer(pos):is_started() then
sign_refresh(pos)
end
if fields.text ~= "" then
minetest.add_entity(pos, "signs:sign")
end
return true
end
minetest.register_node("signs:sign", {
description = "sign",
drawtype = "nodebox",
@ -148,13 +204,14 @@ minetest.register_node("signs:sign", {
return itemstack
end
-- show text input form
context[name] = pos
local text = minetest.get_meta(pos):get_string("text")
minetest.show_formspec(clicker:get_player_name(), "signs:text",
fsc.show(name,
"size[8,8]" ..
"textarea[0.5,0.5;7.5,6.5;text;text;" ..
minetest.formspec_escape(text) .. "]" ..
"button_exit[3.5,7;1,0.5;exit;exit]")
"button_exit[3.5,7;1,0.5;exit;exit]",
pos,
text_callback)
end,
on_destruct = function(pos)
clean_sign_entities(pos)
@ -225,13 +282,14 @@ minetest.register_node("signs:sign_wall", {
return itemstack
end
-- show text input form
context[name] = pos
local text = minetest.get_meta(pos):get_string("text")
minetest.show_formspec(clicker:get_player_name(), "signs:text",
fsc.show(name,
"size[8,8]" ..
"textarea[0.5,0.5;7.5,6.5;text;text;" ..
minetest.formspec_escape(text) .. "]" ..
"button_exit[3.5,7;1,0.5;exit;exit]")
"button_exit[3.5,7;1,0.5;exit;exit]",
pos,
text_callback)
end,
on_destruct = function(pos)
clean_sign_entities(pos)
@ -248,69 +306,6 @@ minetest.register_node("signs:sign_wall", {
end,
})
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "signs:text" then
return false
end
local name = player:get_player_name()
--validate user was allowed to edit signs
if not boxes.can_edit(player) or not context[name] then
log.fs_data(player, name, formname, fields)
return true
end
if not fields.text then
if fields.quit then
return true
end
fields.text = ""
end
-- validate length of sign text does not exceed max
if string.len(fields.text) > 1000 then
fields.text = fields.text:sub(1, 1000)
end
-- check: validate composed texture string does not exceed max
-- max 1000 characters, each character can take up to 17 chars to encode
-- in tex. So the total size is 17000 + "[combine", which easily fits
-- 64k max texture string.
--validate sign pos is actually within the box that player is in
local pos = context[name]
if not boxes.validate_pos(player, pos) then
return true
end
-- verify node is still a sign
local node = minetest.get_node(pos)
if node.name ~= "signs:sign" and node.name ~= "signs:sign_wall" then
-- sign no longer exists. Could be lag.
return true
end
-- erase sign
context[name] = nil
clean_sign_entities(pos)
local meta = minetest.get_meta(pos)
meta:set_string("text", fields.text)
meta:mark_as_private("text")
-- for lobby signs that need a refresh
if minetest.get_node_timer(pos):is_started() then
sign_refresh(pos)
end
if fields.text ~= "" then
minetest.add_entity(pos, "signs:sign")
end
return true
end)
local function make_tex(pos)
local meta = minetest.get_meta(pos)
local text = meta:get_string("dtext")
@ -510,6 +505,24 @@ local icons = {
[18] = "winksmile",
[19] = "xmouth",
}
local function icon_callback(player, fields, context)
if not fields.quit then
local k, _ = next(fields)
local i = tonumber(k) or 0
if not icons[i] then
return true
end
local pos = context.pos
local node = minetest.get_node(pos)
node.name = "signs:icon_" .. icons[i]
minetest.swap_node(pos, node)
end
return true
end
for k, v in ipairs(icons) do
minetest.register_node("signs:icon_".. v, {
description = "sign (" .. v .. ")",
@ -542,7 +555,6 @@ for k, v in ipairs(icons) do
return
end
local name = box.name -- player name
context[name] = pos
local form = "size[9,8]\n"
for kk, vv in ipairs(icons) do
form = form ..
@ -558,10 +570,7 @@ for k, v in ipairs(icons) do
2.0 * math.floor((kk-1)/5) + 1.5 ..
";" .. vv .. "]\n"
end
minetest.show_formspec(name, "signs:icons", form)
--local node = minetest.get_node(pos)
--node.name = "signs:icon_" .. icons[i]
--minetest.swap_node(pos, node)
fsc.show(name, form, {pos = pos}, icon_callback)
end,
groups = {icon = 1}, -- find icons using group:icon
icon_name = v,
@ -569,32 +578,6 @@ for k, v in ipairs(icons) do
})
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "signs:icons" then
return false
end
local name = player:get_player_name()
if not fields.quit then
local k, _ = next(fields)
local i = tonumber(k) or 0
if not icons[i] or not context[name] then
--FIXME formspec data log
minetest.close_formspec(name, "")
return true
end
local pos = context[name]
local node = minetest.get_node(pos)
node.name = "signs:icon_" .. icons[i]
minetest.swap_node(pos, node)
end
context[name] = nil
minetest.close_formspec(name, "")
return true
end)
local stardir = {
[0] = {x = 1, y = 0, z = 0},

View File

@ -1,3 +1,4 @@
mech
rules
log
fsc

View File

@ -5,7 +5,7 @@
]]--
local context = {}
local term = {}
local function get_cmd_params(line)
local cmd = ""
@ -22,7 +22,7 @@ local function get_cmd_params(line)
return cmd, params
end
local help = {
term.help = {
append = "append text to a file",
clear = "clear the output",
echo = "echoes the input back to you",
@ -36,7 +36,24 @@ local help = {
edit = "edits a file in an editor",
}
local commands = {
local function make_formspec(output, prompt)
local f =
"size[12,8]" ..
"field_close_on_enter[input;false]" ..
"textlist[0.4,0.5;11,6;output;"
local c = 1
for part in output:gmatch("[^\r\n]+") do
f = f .. minetest.formspec_escape(part) .. ","
c = c + 1
end
f = f .. minetest.formspec_escape(prompt) .. ";" .. c .. ";false]"
f = f .. "field[0.7,7;11.2,1;input;;]"
return f
end
term.commands = {
clear = function(output, params, c)
return ""
end,
@ -93,11 +110,12 @@ local commands = {
end
end
minetest.show_formspec(c.name, "terminal:edit",
"size[12,8]" ..
fsc.show(c.name, "size[12,8]" ..
"textarea[0.5,0.5;11.5,7.0;text;text;" ..
minetest.formspec_escape(text) .. "]" ..
"button_exit[5.2,7.2;1.6,0.5;exit;Save]")
"button_exit[5.2,7.2;1.6,0.5;exit;Save]",
c,
term.edit)
return false
end,
@ -173,15 +191,15 @@ local commands = {
help = function(output, params, c)
if params ~= "" then
local h, _ = get_cmd_params(params)
if help[h] then
return output .. "\n" .. help[h]
if term.help[h] then
return output .. "\n" .. term.help[h]
else
return output .. "\nError: No help for \"" .. h .. "\""
end
end
local o = ""
local ot = {}
for k, _ in pairs(help) do
for k, _ in pairs(term.help) do
ot[#ot + 1] = k
end
table.sort(ot)
@ -195,76 +213,12 @@ local commands = {
end,
}
local function make_formspec(output, prompt)
local f =
"size[12,8]" ..
"field_close_on_enter[input;false]" ..
"textlist[0.4,0.5;11,6;output;"
local c = 1
for part in output:gmatch("[^\r\n]+") do
f = f .. minetest.formspec_escape(part) .. ","
c = c + 1
end
f = f .. minetest.formspec_escape(prompt) .. ";" .. c .. ";false]"
f = f .. "field[0.7,7;11.2,1;input;;]"
return f
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "terminal:edit" then
return false
end
if not fields.text then
return true
end
local name = player:get_player_name()
local c = context[name]
if not c or not c.pos or not c.output then
log.fs_data(player, name, formname, fields)
return true
end
local output = c.output
if not c.what then
output = output .. "\n" .. "Error: no such file\n"
minetest.show_formspec(name, "terminal:", make_formspec(output, "> "))
return true
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files
files = minetest.parse_json(meta_files) or {}
files[c.what] = fields.text
-- validate it fits
local json = minetest.write_json(files)
if string.len(json) < 49152 then
meta:set_string("files", json)
meta:mark_as_private("files")
output = output .. "\n" .. "Wrote: " .. c.what .. "\n"
else
output = output .. "\n" .. "Error: no space left on device\n"
end
minetest.show_formspec(name, "terminal:", make_formspec(output, "> "))
return true
end)
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "terminal:" then
return false
end
function term.recv(player, fields, context)
-- input validation
local name = player:get_player_name()
local c = context[name]
local c = context
if not c or not c.pos then
log.fs_data(player, name, formname, fields)
log.fs_data(player, name, "terminal:", fields)
return true
end
@ -280,8 +234,11 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
output = output .. "\n" .. line
output = output .. "\nError: no write access"
c.output = output
minetest.show_formspec(name, "terminal:", make_formspec(output, "> "))
return true
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
-- are we writing a file?
if line == "STOP" then
@ -289,8 +246,11 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
c.writing = nil
output = output .. "\n" .. line
c.output = output
minetest.show_formspec(name, "terminal:", make_formspec(output, "> "))
return true
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
@ -318,7 +278,11 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
output = output .. "\n" .. "Error: maximum file length exceeded"
end
c.output = output
minetest.show_formspec(name, "terminal:", make_formspec(output, ""))
fsc.show(name,
make_formspec(output, ""),
c,
term.recv)
return
else
-- else parse cmd
output = output .. "\n> " .. line
@ -328,11 +292,14 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if meta:get_int("locked") == 1 and cmd ~= "unlock" then
output = output .. "\nError: Terminal locked, type \"unlock\" to unlock it"
c.output = output
minetest.show_formspec(name, "terminal:", make_formspec(output, "> "))
return true
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local fn = commands[cmd]
local fn = term.commands[cmd]
if fn then
output = fn(output, params, c)
else
@ -340,25 +307,76 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
if output ~= false then
c.output = output
minetest.show_formspec(name, "terminal:", make_formspec(output, "> "))
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
end
return
end
elseif fields.quit then
context[name] = nil
minetest.sound_play("terminal_power_off", {pos = c.pos})
else
log.fs_data(player, name, formname, fields)
return true
elseif fields.output then
-- CHG events - do not return true
return
end
log.fs_data(player, name, "terminal:", fields)
return true
end
function term.edit(player, fields, context)
if not fields.text then
return true
end
return true
end)
local name = player:get_player_name()
local c = context
if not c or not c.pos or not c.output then
log.fs_data(player, name, "terminal:", fields)
return true
end
local output = c.output
if not c.what then
output = output .. "\n" .. "Error: no such file\n"
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files
files = minetest.parse_json(meta_files) or {}
files[c.what] = fields.text
-- validate it fits
local json = minetest.write_json(files)
if string.len(json) < 49152 then
meta:set_string("files", json)
meta:mark_as_private("files")
output = output .. "\n" .. "Wrote: " .. c.what .. "\n"
else
output = output .. "\n" .. "Error: no space left on device\n"
end
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local terminal_use = function(pos, node, clicker, itemstack, pointed_thing)
if not clicker then
return
end
local name = clicker:get_player_name()
context[name] = {
local context = {
pos = pos,
rw = false,
output = "",
@ -367,10 +385,13 @@ local terminal_use = function(pos, node, clicker, itemstack, pointed_thing)
if boxes.players_editing_boxes[name] or
(not boxes.players_in_boxes[name] and minetest.check_player_privs(clicker, "server")) then
-- allow rw access
context[name].rw = true
context.rw = true
end
-- send formspec to player
minetest.show_formspec(name, "terminal:", make_formspec("", "> "))
fsc.show(name,
make_formspec("", "> "),
context,
term.recv)
minetest.sound_play("terminal_power_on", {pos = pos})
-- trigger on first use
local meta = minetest.get_meta(pos)
@ -380,7 +401,6 @@ local terminal_use = function(pos, node, clicker, itemstack, pointed_thing)
end
end
minetest.register_node("terminal:terminal", {
description = "An interactive terminal console emulator access interface unit controller",
drawtype = "mesh",