diff --git a/init.lua b/init.lua index c6afea5..c602f85 100644 --- a/init.lua +++ b/init.lua @@ -3,9 +3,39 @@ wesh = { name = "wesh", modpath = minetest.get_modpath(minetest.get_current_modname()), vt_size = 72, - player_canvas = {} + player_canvas = {}, + forms = {}, } + +local smartfs = dofile(wesh.modpath .. "/smartfs.lua") + +wesh.forms.capture = smartfs.create("wesh.forms.capture", function(state) + state:size(6, 6) + + local meshname_field = state:field(0.5, 0.5, 4, 1, "meshname", "Enter the name for your mesh") + meshname_field:onKeyEnter(wesh.mesh_capture_confirmed) + meshname_field:setCloseOnEnter(false) + + local capture_button = state:button(4, 0.2, 2, 1, "capture", "Capture") + capture_button:click(wesh.mesh_capture_confirmed) + -- capture_button:setClose(true) + + local cancel_button = state:button(4, 5.2, 2, 1, "cancel", "Cancel") + cancel_button:setClose(true) + + state:checkbox(0.5, 1, "generate_matrix", "Generate backup matrix") + + state:label(0.5, 2, "label_variants", "Select one or more variants:") + + local y = 2.5 + for name, texture in pairs(wesh.variants) do + local chk = state:checkbox(0.5, y, "variant_" .. name, name) + chk:setValue(true) + y = y + 0.5 + end +end) + -- ======================================================================== -- initialization functions -- ======================================================================== @@ -15,7 +45,7 @@ function wesh._init() wesh.gen_prefix = "mesh_" if not minetest.mkdir(wesh.temp_path) then - error("[" .. wesh.name .. "] Unable to create folder " .. wesh.temp_path) + error("[wesh] Unable to create folder " .. wesh.temp_path) end wesh._init_vertex_textures() wesh._init_colors() @@ -27,7 +57,6 @@ function wesh._init() end function wesh._register_canvas_nodes() - minetest.register_on_player_receive_fields(wesh.on_receive_fields) local function register_canvas(index, size, inner) minetest.register_craft({ @@ -266,39 +295,57 @@ end function wesh.canvas_interaction(clicked_pos, node, clicker) -- called when the player right-clicks on a canvas block wesh.player_canvas[clicker:get_player_name()] = { pos = clicked_pos, facedir = node.param2 }; - local formspec = "field[meshname;Enter the name for your mesh;]field_close_on_enter[meshname;false]" - minetest.show_formspec(clicker:get_player_name(), "save_mesh", formspec) + wesh.forms.capture:show(clicker:get_player_name()) end -function wesh.on_receive_fields(player, formname, fields) - if formname == "save_mesh" then - local canvas = wesh.player_canvas[player:get_player_name()] - canvas.node = minetest.get_node_or_nil(canvas.pos) - if not canvas.node then return end - - canvas.size = canvas.node.name:gsub(".*(%d%d)$", "%1") - canvas.size = tonumber(canvas.size) - if not wesh.valid_canvas_sizes[canvas.size] then - canvas.size = 16 +function wesh.mesh_capture_confirmed(button_or_field, state) + local meshname = state:get("meshname"):getText() + local playername = state.player + local canvas = wesh.player_canvas[playername] + canvas.generate_matrix = state:get("generate_matrix"):getValue() + + canvas.chosen_variants = {} + + local no_variants = true + for name, texture in pairs(wesh.variants) do + if state:get("variant_" .. name):getValue() then + canvas.chosen_variants[name] = texture + no_variants = false end + end + + if no_variants then + wesh.notify(playername, "Please choose at least one variant") + return + end + + canvas.node = minetest.get_node_or_nil(canvas.pos) + if not canvas.node then return end - canvas.boundary = {} - wesh.save_new_mesh(canvas, player, fields.meshname) + canvas.size = canvas.node.name:gsub(".*(%d%d)$", "%1") + canvas.size = tonumber(canvas.size) + if not wesh.valid_canvas_sizes[canvas.size] then + canvas.size = 16 + end + + canvas.boundary = {} + if wesh.save_new_mesh(canvas, playername, meshname) then + minetest.close_formspec(playername, "wesh.forms.capture") end end -function wesh.save_new_mesh(canvas, player, description) +function wesh.save_new_mesh(canvas, playername, description) local sanitized_meshname = wesh.check_plain(description) if sanitized_meshname:len() < 3 then - wesh.notify(player, "Mesh name too short, try again (min. 3 chars)") - return + wesh.notify(playername, "Mesh name too short, try again (min. 3 chars)") + return false end local obj_filename = wesh.gen_prefix .. sanitized_meshname .. ".obj" for _, entry in ipairs(wesh.get_all_files()) do if entry == obj_filename then - wesh.notify(player, "Mesh name '" .. description .. "' already taken, pick a new one") - return + wesh.notify(playername, "Mesh name '" .. description .. "' already taken, pick a new one") + return false end end @@ -323,21 +370,21 @@ function wesh.save_new_mesh(canvas, player, description) local f_section = table.concat(wesh.faces, "\n") local meshdata = vt_section .. v_section .. vn_section .. f_section - wesh.save_mesh_to_file(obj_filename, meshdata, description, player, canvas) + return wesh.save_mesh_to_file(obj_filename, meshdata, description, playername, canvas) end -- ======================================================================== -- mesh management helpers -- ======================================================================== -function wesh.save_mesh_to_file(obj_filename, meshdata, description, player, canvas) +function wesh.save_mesh_to_file(obj_filename, meshdata, description, playername, canvas) -- save .obj file local full_filename = wesh.temp_path .. "/" .. obj_filename local file, errmsg = io.open(full_filename, "wb") if not file then - wesh.notify(player, "Unable to write to file '" .. obj_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) - return + wesh.notify(playername, "Unable to write to file '" .. obj_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) + return false end file:write(meshdata) file:close() @@ -347,25 +394,27 @@ function wesh.save_mesh_to_file(obj_filename, meshdata, description, player, can local full_data_filename = wesh.temp_path .. "/" .. data_filename local file, errmsg = io.open(full_data_filename, "wb") if not file then - wesh.notify(player, "Unable to write to file '" .. data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) - return + wesh.notify(playername, "Unable to write to file '" .. data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) + return false end file:write(wesh.prepare_data_file(description, canvas)) file:close() - -- save .matrix.dat file - local matrix_data_filename = obj_filename .. ".matrix.dat" - local full_matrix_data_filename = wesh.temp_path .. "/" .. matrix_data_filename - local file, errmsg = io.open(full_matrix_data_filename, "wb") - if not file then - wesh.notify(player, "Unable to write to file '" .. matrix_data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) - return + if canvas.generate_matrix then + -- save .matrix.dat file + local matrix_data_filename = obj_filename .. ".matrix.dat" + local full_matrix_data_filename = wesh.temp_path .. "/" .. matrix_data_filename + local file, errmsg = io.open(full_matrix_data_filename, "wb") + if not file then + wesh.notify(playername, "Unable to write to file '" .. matrix_data_filename .. "' from '" .. wesh.temp_path .. "' - error: " .. errmsg) + return false + end + file:write(minetest.serialize(wesh.matrix)) + file:close() end - file:write(minetest.serialize(wesh.matrix)) - file:close() - - wesh.notify(player, "Mesh saved to '" .. obj_filename .. "' in '" .. wesh.temp_path .. "', reload the world to move them to the mod folder and enable them") + wesh.notify(playername, "Mesh saved to '" .. obj_filename .. "' in '" .. wesh.temp_path .. "', reload the world to move them to the mod folder and enable them") + return true end function wesh.prepare_data_file(description, canvas) @@ -374,9 +423,10 @@ function wesh.prepare_data_file(description, canvas) for _, box in ipairs(canvas.boxes) do table.insert(boxes, wesh.box_to_collision_box(box, canvas.size)) end + local data = { description = description, - variants = wesh.variants, + variants = canvas.chosen_variants, collision_box = { type = "fixed", fixed = boxes, @@ -780,12 +830,8 @@ function wesh.merge_tables(t1, t2) end end -function wesh.notify(player, message) - local formspec = "size[10,5]textarea[1,1;8,3;notice;Notice;" .. minetest.formspec_escape(message) .. "]" - .. "button_exit[6,4;3,0;exit;Okay]" - local playername = player:get_player_name() - minetest.show_formspec(playername, "notice_form", formspec) - minetest.chat_send_player(playername, "[" .. wesh.name .. "] " .. message) +function wesh.notify(playername, message) + minetest.chat_send_player(playername, "[wesh] " .. message) end function wesh.out_of_bounds(pos, canv_size) diff --git a/smartfs.lua b/smartfs.lua new file mode 100644 index 0000000..fa6af7a --- /dev/null +++ b/smartfs.lua @@ -0,0 +1,1422 @@ +--------------------------- +-- SmartFS: Smart Formspecs +-- License: CC0 or WTFPL +-- by Rubenwardy +--------------------------- + +local smartfs = { + _fdef = {}, + _edef = {}, + _ldef = {}, + opened = {}, + inv = {} +} + +local function boolToStr(v) + return v and "true" or "false" +end + +-- the smartfs() function +function smartfs.__call(self, name) + return smartfs.get(name) +end + +function smartfs.get(name) + return smartfs._fdef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a new form and adds elements to it by running the function. Use before Minetest loads. (like minetest.register_node) +------------------------------------------------------ +-- Register forms and elements +function smartfs.create(name, onload) + assert(not smartfs._fdef[name], + "SmartFS - (Error) Form "..name.." already exists!") + assert(not smartfs.loaded or smartfs._loaded_override, + "SmartFS - (Error) Forms should be declared while the game loads.") + + smartfs._fdef[name] = { + form_setup_callback = onload, + name = name, + show = smartfs._show_, + attach_to_node = smartfs._attach_to_node_ + } + + return smartfs._fdef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a new element type +------------------------------------------------------ +function smartfs.element(name, data) + assert(not smartfs._edef[name], + "SmartFS - (Error) Element type "..name.." already exists!") + + assert(data.onCreate, "element requires onCreate method") + smartfs._edef[name] = data + return smartfs._edef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a dynamic form. Returns state +------------------------------------------------------ +function smartfs.dynamic(name,player) + if not smartfs._dynamic_warned then + smartfs._dynamic_warned = true + minetest.log("warning", "SmartFS - (Warning) On the fly forms are being used. May cause bad things to happen") + end + local statelocation = smartfs._ldef.player._make_state_location_(player) + local state = smartfs._makeState_({name=name}, nil, statelocation, player) + smartfs.opened[player] = state + return state +end + +------------------------------------------------------ +-- Smartfs Interface - Returns the name of an installed and supported inventory mod that will be used above, or nil +------------------------------------------------------ +function smartfs.inventory_mod() + if minetest.global_exists("unified_inventory") then + return "unified_inventory" + elseif minetest.global_exists("inventory_plus") then + return "inventory_plus" + else + return nil + end +end + +------------------------------------------------------ +-- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success. +------------------------------------------------------ +function smartfs.add_to_inventory(form, icon, title) + local ldef + local invmod = smartfs.inventory_mod() + if invmod then + ldef = smartfs._ldef[invmod] + else + return false + end + return ldef.add_to_inventory(form, icon, title) +end + +------------------------------------------------------ +-- Smartfs Interface - Set the form as players inventory +------------------------------------------------------ +function smartfs.set_player_inventory(form) + smartfs._ldef.inventory.set_inventory(form) +end +------------------------------------------------------ +-- Smartfs Interface - Allows you to use smartfs.create after the game loads. Not recommended! +------------------------------------------------------ +function smartfs.override_load_checks() + smartfs._loaded_override = true +end + +------------------------------------------------------ +-- Smartfs formspec locations +------------------------------------------------------ +-- Unified inventory plugin +smartfs._ldef.unified_inventory = { + add_to_inventory = function(form, icon, title) + unified_inventory.register_button(form.name, { + type = "image", + image = icon, + }) + unified_inventory.register_page(form.name, { + get_formspec = function(player, formspec) + local name = player:get_player_name() + local statelocation = smartfs._ldef.unified_inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + return {formspec = state:_buildFormspec_(false)} + else + return nil + end + end + }) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + unified_inventory.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state.def.name) + end + } + end +} + +-- Inventory plus plugin +smartfs._ldef.inventory_plus = { + add_to_inventory = function(form, icon, title) + minetest.register_on_joinplayer(function(player) + inventory_plus.register_button(player, form.name, title) + end) + minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "" and fields[form.name] then + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory_plus._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end + end) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + inventory_plus.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state:_buildFormspec_(true)) + end + } + end +} + +-- Show to player +smartfs._ldef.player = { + _make_state_location_ = function(player) + return { + type = "player", + player = player, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + if (not state.closed) and (not state.obsolete) then + minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true)) + end + end + end, state) -- state given as reference. Maybe additional updates are done in the meantime or the form is obsolete + end + end + } + end +} + +-- Standalone inventory +smartfs._ldef.inventory = { + set_inventory = function(form) + if sfinv and sfinv.enabled then + sfinv.enabled = nil + end + minetest.register_on_joinplayer(function(player) + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end) + minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + smartfs.inv[name].obsolete = true + smartfs.inv[name] = nil + end) + end, + _make_state_location_ = function(name) + return { + type = "inventory", + player = name, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + local player = minetest.get_player_by_name(state.location.player) + --print("smartfs formspec:", state:_buildFormspec_(true)) + player:set_inventory_formspec(state:_buildFormspec_(true)) + end + end, state) + end + end + } + end +} + +-- Node metadata +smartfs._ldef.nodemeta = { + _make_state_location_ = function(nodepos) + return { + type = "nodemeta", + pos = nodepos, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + local meta = minetest.get_meta(state.location.pos) + meta:set_string("formspec", state:_buildFormspec_(true)) + meta:set_string("smartfs_name", state.def.name) + meta:mark_as_private("smartfs_name") + end + end, state) + end + end, + } + end +} + +-- Sub-container (internally used) +smartfs._ldef.container = { + _make_state_location_ = function(element) + local self = { + type = "container", + containerElement = element, + parentState = element.root + } + if self.parentState.location.type == "container" then + self.rootState = self.parentState.location.rootState + else + self.rootState = self.parentState + end + return self + end +} + +------------------------------------------------------ +-- Minetest Interface - on_receive_fields callback can be used in minetest.register_node for nodemeta forms +------------------------------------------------------ +function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, params) + local meta = minetest.get_meta(nodepos) + local nodeform = meta:get_string("smartfs_name") + if not nodeform then + print("SmartFS - (Warning) smartfs.nodemeta_on_receive_fields for node without smarfs data") + return false + end + + -- get the currentsmartfs state + local opened_id = minetest.pos_to_string(nodepos) + local state + local form = smartfs.get(nodeform) + if not smartfs.opened[opened_id] or -- If opened first time + smartfs.opened[opened_id].def.name ~= nodeform or -- Or form is changed + smartfs.opened[opened_id].obsolete then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + state = smartfs._makeState_(form, params, statelocation) + if smartfs.opened[opened_id] then + smartfs.opened[opened_id].obsolete = true + end + smartfs.opened[opened_id] = state + form.form_setup_callback(state) + else + state = smartfs.opened[opened_id] + end + + -- Set current sender check for multiple users on node + local name + if sender then + name = sender:get_player_name() + state.players:connect(name) + end + + -- take the input + state:_sfs_on_receive_fields_(name, fields) + + -- Reset form if all players disconnected + if sender and not state.players:get_first() and not state.obsolete then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local resetstate = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(resetstate) ~= false then + resetstate:show() + end + smartfs.opened[opened_id] = nil + end +end + +------------------------------------------------------ +-- Minetest Interface - on_player_receive_fields callback in case of inventory or player +------------------------------------------------------ +minetest.register_on_player_receive_fields(function(player, formname, fields) + local name = player:get_player_name() + if smartfs.opened[name] and smartfs.opened[name].location.type == "player" then + if smartfs.opened[name].def.name == formname then + local state = smartfs.opened[name] + state:_sfs_on_receive_fields_(name, fields) + + -- disconnect player if form closed + if not state.players:get_first() then + smartfs.opened[name].obsolete = true + smartfs.opened[name] = nil + end + end + elseif smartfs.inv[name] and smartfs.inv[name].location.type == "inventory" then + local state = smartfs.inv[name] + state:_sfs_on_receive_fields_(name, fields) + end + return false +end) + +------------------------------------------------------ +-- Minetest Interface - Notify loading of smartfs is done +------------------------------------------------------ +minetest.after(0, function() + smartfs.loaded = true +end) + +------------------------------------------------------ +-- Form Interface [linked to form:show()] - Shows the form to a player +------------------------------------------------------ +function smartfs._show_(form, name, params) + assert(form) + assert(type(name) == "string", "smartfs: name needs to be a string") + assert(minetest.get_player_by_name(name), "player does not exist") + local statelocation = smartfs._ldef.player._make_state_location_(name) + local state = smartfs._makeState_(form, params, statelocation, name) + if form.form_setup_callback(state) ~= false then + if smartfs.opened[name] then -- set maybe previous form to obsolete + smartfs.opened[name].obsolete = true + end + smartfs.opened[name] = state + state:show() + end + return state +end + +------------------------------------------------------ +-- Form Interface [linked to form:attach_to_node()] - Attach a formspec to a node meta +------------------------------------------------------ +function smartfs._attach_to_node_(form, nodepos, params) + assert(form) + assert(nodepos and nodepos.x) + + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local state = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(state) ~= false then + local opened_id = minetest.pos_to_string(nodepos) + if smartfs.opened[opened_id] then -- set maybe previous form to obsolete + smartfs.opened[opened_id].obsolete = true + end + state:show() + end + return state +end + +------------------------------------------------------ +-- Smartfs Framework - create a form object (state) +------------------------------------------------------ +function smartfs._makeState_(form, params, location, newplayer) + ------------------------------------------------------ + -- State - -- Object to manage players + ------------------------------------------------------ + local function _make_players_(newplayer) + local self = { + _list = {} + } + function self.connect(self, player) + self._list[player] = true + end + function self.disconnect(self, player) + self._list[player] = nil + end + function self.get_first(self) + return next(self._list) + end + if newplayer then + self:connect(newplayer) + end + return self + end + + + ------------------------------------------------------ + -- State - create returning state object + ------------------------------------------------------ + return { + _ele = {}, + def = form, + players = _make_players_(newplayer), + location = location, + is_inv = (location.type == "inventory"), -- obsolete. Please use location.type="inventory" instead + player = newplayer, -- obsolete. Please use location.player + param = params or {}, + get = function(self,name) + return self._ele[name] + end, + close = function(self) + self.closed = true + end, + getSize = function(self) + return self._size + end, + size = function(self,w,h) + self._size = {w=w,h=h} + end, + setSize = function(self,w,h) + self._size = {w=w,h=h} + end, + getNamespace = function(self) + local ref = self + local namespace = "" + while ref.location.type == "container" do + namespace = ref.location.containerElement.name.."#"..namespace + ref = ref.location.parentState -- step near to the root + end + return namespace + end, + _buildFormspec_ = function(self,size) + local res = "" + if self._size and size then + res = "size["..self._size.w..","..self._size.h.."]" + end + for key,val in pairs(self._ele) do + if val:getVisible() then + res = res .. val:getBackgroundString() .. val:build() .. val:getTooltipString() + end + end + return res + end, + show = location._show_, + _get_element_recursive_ = function(self, field) + local topfield + for z in field:gmatch("[^#]+") do + topfield = z + break + end + local element = self._ele[topfield] + if element and field == topfield then + return element + elseif element then + if element._getSubElement_ then + local rel_field = string.sub(field, string.len(topfield)+2) + return element:_getSubElement_(rel_field) + else + return element + end + else + return nil + end + end, + -- process onInput hook for the state + _sfs_process_oninput_ = function(self, fields, player) + if self._onInput then + self:_onInput(fields, player) + end + -- recursive all onInput hooks on visible containers + for elename, eledef in pairs(self._ele) do + if eledef.getContainerState and eledef:getVisible() then + eledef:getContainerState():_sfs_process_oninput_(fields, player) + end + end + end, + -- Receive fields and actions from formspec + _sfs_on_receive_fields_ = function(self, player, fields) + + local fields_todo = {} + for field, value in pairs(fields) do + local element = self:_get_element_recursive_(field) + if element then + fields_todo[field] = { element = element, value = value } + end + end + + for field, todo in pairs(fields_todo) do + todo.element:setValue(todo.value) + end + + self:_sfs_process_oninput_(fields, player) + + for field, todo in pairs(fields_todo) do + if todo.element.submit then + todo.element:submit(todo.value, player) + end + end + -- handle key_enter + if fields.key_enter and fields.key_enter_field then + local element = self:_get_element_recursive_(fields.key_enter_field) + if element and element.submit_key_enter then + element:submit_key_enter(fields[fields.key_enter_field], player) + end + end + + if not fields.quit and not self.closed and not self.obsolete then + self:show() + else + self.players:disconnect(player) + if not fields.quit and self.closed and not self.obsolete then + --closed by application (without fields.quit). currently not supported, see: https://github.com/minetest/minetest/pull/4675 + minetest.show_formspec(player,"","size[5,1]label[0,0;Formspec closing not yet created!]") + end + end + return true + end, + onInput = function(self, func) + self._onInput = func -- (fields, player) + end, + load = function(self,file) + local file = io.open(file, "r") + if file then + local table = minetest.deserialize(file:read("*all")) + if type(table) == "table" then + if table.size then + self._size = table.size + end + for key,val in pairs(table.ele) do + self:element(val.type,val) + end + return true + end + end + return false + end, + save = function(self,file) + local res = {ele={}} + + if self._size then + res.size = self._size + end + + for key,val in pairs(self._ele) do + res.ele[key] = val.data + end + + local file = io.open(file, "w") + if file then + file:write(minetest.serialize(res)) + file:close() + return true + end + return false + end, + setparam = function(self,key,value) + if not key then return end + self.param[key] = value + return true + end, + getparam = function(self,key,default) + if not key then return end + return self.param[key] or default + end, + element = function(self,typen,data) + local type = smartfs._edef[typen] + assert(type, "Element type "..typen.." does not exist!") + assert(not self._ele[data.name], "Element "..data.name.." already exists") + + data.type = typen + local ele = { + name = data.name, + root = self, + data = data, + remove = function(self) + self.root._ele[self.name] = nil + end, + setPosition = function(self,x,y) + self.data.pos = {x=x,y=y} + end, + getPosition = function(self) + return self.data.pos + end, + setSize = function(self,w,h) + self.data.size = {w=w,h=h} + end, + getSize = function(self) + return self.data.size + end, + setVisible = function(self, visible) + if visible == nil then + self.data.visible = true + else + self.data.visible = visible + end + end, + getVisible = function(self) + return self.data.visible + end, + getAbsName = function(self) + return self.root:getNamespace()..self.name + end, + setBackground = function(self, image) + self.data.background = image + end, + getBackground = function(self) + return self.data.background + end, + getBackgroundString = function(self) + if self.data.background then + local size = self:getSize() + if size then + return "background[".. + self.data.pos.x..","..self.data.pos.y..";".. + size.w..","..size.h..";".. + self.data.background.."]" + else + return "" + end + else + return "" + end + end, + setValue = function(self, value) + self.data.value = value + end, + setTooltip = function(self,text) + self.data.tooltip = minetest.formspec_escape(text) + end, + getTooltip = function(self) + return self.data.tooltip + end, + getTooltipString = function(self) + if self.data.tooltip then + return "tooltip["..self:getAbsName()..";"..self:getTooltip().."]" + else + return "" + end + end, + } + + ele.data.visible = true --visible by default + + for key, val in pairs(type) do + ele[key] = val + end + + self._ele[data.name] = ele + + type.onCreate(ele) + + return self._ele[data.name] + end, + + ------------------------------------------------------ + -- State - Element Constructors + ------------------------------------------------------ + button = function(self, x, y, w, h, name, text, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + closes = exitf or false + }) + end, + image_button = function(self, x, y, w, h, name, text, image, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + image = image, + closes = exitf or false + }) + end, + item_image_button = function(self, x, y, w, h, name, text, item, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + item = item, + closes = exitf or false + }) + end, + label = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = false + }) + end, + vertlabel = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = true + }) + end, + toggle = function(self, x, y, w, h, name, list) + return self:element("toggle", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + id = 1, + list = list + }) + end, + field = function(self, x, y, w, h, name, label) + return self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + end, + pwdfield = function(self, x, y, w, h, name, label) + local res = self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + res:isPassword(true) + return res + end, + textarea = function(self, x, y, w, h, name, label) + local res = self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + res:isMultiline(true) + return res + end, + image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "image" + }) + end, + background = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + background = img, + imgtype = "background" + }) + end, + item_image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "item" + }) + end, + checkbox = function(self, x, y, name, label, selected) + return self:element("checkbox", { + pos = {x=x, y=y}, + name = name, + value = selected, + label = label + }) + end, + listbox = function(self, x, y, w, h, name, selected, transparent) + return self:element("list", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + selected = selected, + transparent = transparent + }) + end, + dropdown = function(self, x, y, w, h, name, selected) + return self:element("dropdown", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + selected = selected + }) + end, + inventory = function(self, x, y, w, h, name) + return self:element("inventory", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name + }) + end, + container = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = false + }) + end, + view = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = true + }) + end, + } +end + +----------------------------------------------------------------- +------------------------- ELEMENTS ---------------------------- +----------------------------------------------------------------- + +smartfs.element("button", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "button needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "button needs valid size") + assert(self.name, "button needs name") + assert(self.data.value, "button needs label") + end, + build = function(self) + local specstring + if self.data.image then + if self.data.closes then + specstring = "image_button_exit[" + else + specstring = "image_button[" + end + elseif self.data.item then + if self.data.closes then + specstring = "item_image_button_exit[" + else + specstring = "item_image_button[" + end + else + if self.data.closes then + specstring = "button_exit[" + else + specstring = "button[" + end + end + + specstring = specstring .. + self.data.pos.x..","..self.data.pos.y..";".. + self.data.size.w..","..self.data.size.h..";" + if self.data.image then + specstring = specstring..self.data.image..";" + elseif self.data.item then + specstring = specstring..self.data.item..";" + end + specstring = specstring..self:getAbsName()..";".. + minetest.formspec_escape(self.data.value).."]" + return specstring + end, + submit = function(self, field, player) + if self._click then + self:_click(self.root, player) + end + end, + onClick = function(self,func) + self._click = func + end, + click = function(self,func) + self._click = func + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end, + setImage = function(self,image) + self.data.image = image + self.data.item = nil + end, + getImage = function(self) + return self.data.image + end, + setItem = function(self,item) + self.data.item = item + self.data.image = nil + end, + getItem = function(self) + return self.data.item + end, + setClose = function(self,bool) + self.data.closes = bool + end, + getClose = function(self) + return self.data.closes or false + end +}) + +smartfs.element("toggle", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "toggle needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "toggle needs valid size") + assert(self.name, "toggle needs name") + assert(self.data.list, "toggle needs data") + end, + build = function(self) + return "button[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.list[self.data.id]).. + "]" + end, + submit = function(self, field, player) + self.data.id = self.data.id + 1 + if self.data.id > #self.data.list then + self.data.id = 1 + end + if self._tog then + self:_tog(self.root, player) + end + end, + onToggle = function(self,func) + self._tog = func + end, + setId = function(self,id) + self.data.id = id + end, + getId = function(self) + return self.data.id + end, + getText = function(self) + return self.data.list[self.data.id] + end +}) + +smartfs.element("label", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "label needs valid pos") + assert(self.data.value, "label needs text") + end, + build = function(self) + local specstring + if self.data.vertical then + specstring = "vertlabel[" + else + specstring = "label[" + end + return specstring.. + self.data.pos.x..","..self.data.pos.y.. + ";".. + minetest.formspec_escape(self.data.value).. + "]" + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end +}) + +smartfs.element("field", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "field needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "field needs valid size") + assert(self.name, "field needs name") + self.data.value = self.data.value or "" + self.data.label = self.data.label or "" + end, + build = function(self) + if self.data.ml then + return "textarea[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]" + elseif self.data.pwd then + return "pwdfield[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + "]".. + self:getCloseOnEnterString() + else + return "field[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]".. + self:getCloseOnEnterString() + end + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end, + isPassword = function(self,bool) + self.data.pwd = bool + end, + isMultiline = function(self,bool) + self.data.ml = bool + end, + getCloseOnEnterString = function(self) + if self.close_on_enter == nil then + return "" + else + return "field_close_on_enter["..self:getAbsName()..";"..tostring(self.close_on_enter).."]" + end + end, + setCloseOnEnter = function(self, value) + self.close_on_enter = value + end, + getCloseOnEnter = function(self) + return self.close_on_enter + end, + submit_key_enter = function(self, field, player) + if self._key_enter then + self:_key_enter(self.root, player) + end + end, + onKeyEnter = function(self,func) + self._key_enter = func + end, +}) + +smartfs.element("image", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "image needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "image needs valid size") + self.data.value = self.data.value or "" + end, + build = function(self) + if self.data.imgtype == "background" then + return "" -- handled in _buildFormspec_ trough getBackgroundString() + elseif self.data.imgtype == "item" then + return "item_image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + else + return "image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + end + end, + setImage = function(self,text) + if self.data.imgtype == "background" then + self.data.background = text + else + self:setValue(text) + end + end, + getImage = function(self) + if self.data.imgtype == "background" then + return self.data.background + else + return self.data.value + end + end +}) + +smartfs.element("checkbox", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "checkbox needs valid pos") + assert(self.name, "checkbox needs name") + self.data.value = minetest.is_yes(self.data.value) + self.data.label = self.data.label or "" + end, + build = function(self) + return "checkbox[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";" .. boolToStr(self.data.value) .."]" + end, + submit = function(self, field, player) + -- call the toggle function if defined + if self._tog then + self:_tog(self.root, player) + end + end, + setValue = function(self, value) + self.data.value = minetest.is_yes(value) + end, + getValue = function(self) + return self.data.value + end, + onToggle = function(self,func) + self._tog = func + end, +}) + +smartfs.element("list", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") + assert(self.name, "list needs name") + self.data.value = minetest.is_yes(self.data.value) + self.data.items = self.data.items or {} + end, + build = function(self) + if not self.data.items then + self.data.items = {} + end + return "textlist[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(self.data.items, ",").. + ";".. + tostring(self.data.selected or "").. + ";".. + tostring(self.data.transparent or "false").."]" + end, + submit = function(self, field, player) + local _type = string.sub(field,1,3) + local index = tonumber(string.sub(field,5)) + self.data.selected = index + if _type == "CHG" and self._click then + self:_click(self.root, index, player) + elseif _type == "DCL" and self._doubleClick then + self:_doubleClick(self.root, index, player) + end + end, + onClick = function(self, func) + self._click = func + end, + click = function(self, func) + self._click = func + end, + onDoubleClick = function(self, func) + self._doubleClick = func + end, + doubleclick = function(self, func) + self._doubleClick = func + end, + addItem = function(self, item) + table.insert(self.data.items, minetest.formspec_escape(item)) + -- return the index of item. It is the last one + return #self.data.items + end, + removeItem = function(self,idx) + table.remove(self.data.items,idx) + end, + getItem = function(self, idx) + return self.data.items[idx] + end, + popItem = function(self) + local item = self.data.items[#self.data.items] + table.remove(self.data.items) + return item + end, + clearItems = function(self) + self.data.items = {} + end, + setSelected = function(self,idx) + self.data.selected = idx + end, + getSelected = function(self) + return self.data.selected + end, + getSelectedItem = function(self) + return self:getItem(self:getSelected()) + end, +}) + +smartfs.element("dropdown", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "dropdown needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "dropdown needs valid size") + assert(self.name, "dropdown needs name") + self.data.items = self.data.items or {} + self.data.selected = self.data.selected or 1 + self.data.value = "" + end, + build = function(self) + return "dropdown[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(self.data.items, ",").. + ";".. + tostring(self:getSelected()).. + "]" + end, + submit = function(self, field, player) + self:getSelected() + if self._select then + self:_select(self.root, field, player) + end + end, + onSelect = function(self, func) + self._select = func + end, + addItem = function(self, item) + table.insert(self.data.items, item) + if #self.data.items == self.data.selected then + self.data.value = item + end + -- return the index of item. It is the last one + return #self.data.items + end, + removeItem = function(self,idx) + table.remove(self.data.items,idx) + end, + getItem = function(self, idx) + return self.data.items[idx] + end, + popItem = function(self) + local item = self.data.items[#self.data.items] + table.remove(self.data.items) + return item + end, + clearItems = function(self) + self.data.items = {} + end, + setSelected = function(self,idx) + self.data.selected = idx + self.data.value = self:getItem(idx) or "" + end, + getSelected = function(self) + self.data.selected = 1 + if #self.data.items > 1 then + for i = 1, #self.data.items do + if self.data.items[i] == self.data.value then + self.data.selected = i + end + end + end + return self.data.selected + end, + getSelectedItem = function(self) + return self.data.value + end, +}) + +smartfs.element("inventory", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") + assert(self.name, "list needs name") + end, + build = function(self) + return "list[".. + (self.data.invlocation or "current_player") .. + ";".. + self.name.. --no namespacing + ";".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + (self.data.index or "") .. + "]" + end, + -- available inventory locations + -- "current_player": Player to whom the menu is shown + -- "player:": Any player + -- "nodemeta:,,": Any node metadata + -- "detached:": A detached inventory + -- "context" does not apply to smartfs, since there is no node-metadata as context available + setLocation = function(self,invlocation) + self.data.invlocation = invlocation + end, + getLocation = function(self) + return self.data.invlocation or "current_player" + end, + usePosition = function(self, pos) + self.data.invlocation = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z) + end, + usePlayer = function(self, name) + self.data.invlocation = "player:" .. name + end, + useDetached = function(self, name) + self.data.invlocation = "detached:" .. name + end, + setIndex = function(self,index) + self.data.index = index + end, + getIndex = function(self) + return self.data.index + end +}) + +smartfs.element("code", { + onCreate = function(self) + self.data.code = self.data.code or "" + end, + build = function(self) + if self._build then + self:_build() + end + + return self.data.code + end, + submit = function(self, field, player) + if self._sub then + self:_sub(self.root, field, player) + end + end, + onSubmit = function(self,func) + self._sub = func + end, + onBuild = function(self,func) + self._build = func + end, + setCode = function(self,code) + self.data.code = code + end, + getCode = function(self) + return self.data.code + end +}) + +smartfs.element("container", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "container needs valid pos") + assert(self.name, "container needs name") + local statelocation = smartfs._ldef.container._make_state_location_(self) + self._state = smartfs._makeState_(nil, self.root.param, statelocation) + end, + + -- redefinitions. The size is not handled by data.size but by container-state:size + setSize = function(self,w,h) + self:getContainerState():setSize(w,h) + end, + getSize = function(self) + return self:getContainerState():getSize() + end, + + -- element interface methods + build = function(self) + if self.data.relative ~= true then + return "container["..self.data.pos.x..","..self.data.pos.y.."]".. + self:getContainerState():_buildFormspec_(false).. + "container_end[]" + else + return self:getContainerState():_buildFormspec_(false) + end + end, + getContainerState = function(self) + return self._state + end, + _getSubElement_ = function(self, field) + return self:getContainerState():_get_element_recursive_(field) + end, +}) + +return smartfs \ No newline at end of file