diff --git a/crafting.lua b/crafting.lua new file mode 100644 index 0000000..6905163 --- /dev/null +++ b/crafting.lua @@ -0,0 +1,256 @@ + +local registered_group_items = { + mesecon_conductor_craftable = "mesecons:wire_00000000_off", + stone = "default:cobble", + wood = "default:wood", + book = "default:book", + sand = "default:sand", + leaves = "default:leaves", + tree = "default:tree", + vessel = "vessels:glass_bottle", + wool = "wool:white", +} + +local function craft_preview(state) + for x = 1, 3 do + for y = 1, 3 do + state:item_image((x-1)/2,(y-1)/2,0.5,0.5,"craft:"..x..":"..y,nil):setIsHidden(true) + end + end +end + +local function update_craft_preview(state, craft) + local prevstate = state:get("craft_preview"):getContainerState() + for x = 1, 3 do + for y = 1, 3 do + local item = nil + if craft then + if not craft.width or craft.width == 0 then + item = craft.items[(y-1)*3+x] + elseif x <= craft.width then + item = craft.items[(y-1)*craft.width+x] + end + end + local img = prevstate:get("craft:"..x..":"..y) + if item then + if item:sub(1, 6) == "group:" then + local group_name = item:sub(7) + item = registered_group_items[item:sub(7)] + if not item then + for name, def in pairs(minetest.registered_items) do + if def.groups[group_name] or 0 ~= 0 then + registered_group_items[group_name] = name + item = name + end + end + end + end + img:setImage(item) + img:setIsHidden(false) + else + img:setIsHidden(true) + end + end + end +end + +local function update_listview(state) + local listbox = state:get("list") + local filtermode = state:get("filtermode") + + local inventory = minetest.get_player_by_name(state.location.rootState.location.player):get_inventory() + local invlist = inventory:get_list("main") + local selection = listbox:getSelectedItem() + listbox:clearItems() + listbox:setSelected(1) + state.param.list = {} + for name, def in pairs(minetest.registered_items) do + if def.description and def.description ~= "" then + local recipelist = minetest.get_all_craft_recipes(name) + if recipelist then + local one_item = false + local all_items = false + local selected_info + for _, info in ipairs(recipelist) do + all_items = true + for idx, item in pairs(info.items) do + local in_inventory = false + if item:sub(1, 6) == "group:" then + local group_name = item:sub(7) + for _, stack in ipairs(invlist) do + local stackitemname = stack:get_name() + if minetest.registered_items[stackitemname] and + (minetest.registered_items[stackitemname].groups[group_name] or 0 ~= 0) then + in_inventory = true + info.items[idx] = stackitemname + registered_group_items[group_name] = stackitemname + end + end + elseif minetest.registered_items[item] and inventory:contains_item("main", item) then + in_inventory = true + end + if in_inventory == true then + one_item = true + selected_info = info + else + all_items = false + end + end + if all_items == true then + selected_info = info + break + end + end + if all_items == true or + ( one_item == true and filtermode:getText() == "next" ) then + local text = def.description.." ("..name..")" + local id = listbox:addItem(text) + state.param.list[id] = {info = selected_info, name = name, def = def} + if selection == text then + listbox:setSelected(id) + end + end + end + end + end +end + +local function update_icons_list(state) + local selected_id = state:get("list"):getSelected() + local liststate = state:get("iconslist"):getContainerState() + + if selected_id and selected_id > 18 then + selected_id = selected_id - 18 + else + selected_id = 1 + end + state.param.icons_list_start = selected_id + + for id = selected_id, selected_id+41 do + local info = state.param.list[id] + local button = liststate:get(tostring(id-selected_id+1)) + if info then + button:setIsHidden(false) + button:setItem(info.name) + else + button:setIsHidden(true) + end + end + + if selected_id > 1 then + local button = liststate:get("1") + button:setIsHidden(false) + button:setImage("left_arrow.png") + end + + if state.param.list[selected_id+41] then + local button = liststate:get("42") + button:setIsHidden(false) + button:setImage("right_arrow.png") + end + +end + + +local function crafting_callback(state) + --Inventorys / left site + state:inventory(0, 4, 8, 4,"main") + state:inventory(0.2, 0.5, 3, 3,"craft") + state:inventory(3.4, 2.5, 1, 1,"craftpreview") + state:background(0.1, 0.1, 4.7, 3.8, "img1", "menu_bg.png") + + local listbox = state:listbox(8,0,5.8,6.9,"list") + listbox:onClick(function(self, state, index, playername) + local selected = state.param.list[index] + if selected then + state:get("info1"):setText(selected.def.description) + state:get("info2"):setText("("..selected.name..")") + state:get("info3"):setText("crafting type: "..selected.info.type) + local res = state:get("craft_result") + res:setImage(selected.name) + res:setIsHidden(false) + state:get("craft_preview"):setIsHidden(false) + update_craft_preview(state, selected.info) + else + state:get("info1"):setText("") + state:get("info2"):setText("") + state:get("info3"):setText("") + state:get("craft_preview"):setIsHidden(true) + state:get("craft_result"):setIsHidden(true) + update_craft_preview(state, nil) + end + end) + listbox:setIsHidden(true) + local iconslist = state:container(8,0.5,"iconslist") + local liststate = iconslist:getContainerState() + for x = 1,6 do + for y=1,7 do + local button = liststate:item_image_button(x-1,y-1,1,1,tostring(x+(y-1)*6),"","") + button:setIsHidden(true) + button:onClick(function(self, state, player) + local newsel = tonumber(self.name) + local listbox = state.location.parentState:get("list") + local selection = listbox:getSelected() + if newsel == 1 and state.param.icons_list_start > 1 then + selection = state.param.icons_list_start + listbox:setSelected(selection) + update_icons_list(state.location.parentState) + elseif newsel == 42 and state.param.list[state.param.icons_list_start+41] then + selection = state.param.icons_list_start + 36 + listbox:setSelected(selection) + update_icons_list(state.location.parentState) + else + selection = state.param.icons_list_start+newsel-1 + listbox:setSelected(selection) + listbox:_click(listbox.root, selection, player) + end + end) + end + end + + --buttons above list / or icons view + state:toggle(8, 7.2, 2, 0.5,"filtermode",{"current", "next"}):onToggle(function(self, state, playername) + update_listview(state) + if state:get("viewmode"):getText() == "icons" then + update_icons_list(state) + end + end) + state:toggle(10,7.2, 2, 0.5,"viewmode",{"icons", "list"}):onToggle(function(self, state, playername) + local list = state:get("list") + local iconslist = state:get("iconslist") + if self:getText() == "icons" then + list:setIsHidden(true) + iconslist:setIsHidden(false) + update_icons_list(state) + else + list:setIsHidden(false) + iconslist:setIsHidden(true) + end + end) + + local refresh_button = state:button(12, 7.2, 2, 0.5, "refresh", "Refresh") + refresh_button:onClick(function(self, state, player) + update_listview(state) + if state:get("viewmode"):getText() == "icons" then + update_icons_list(state) + end + end) + + -- preview part + state:label(5,0,"info1", "") + state:label(5,0.5,"info2", "") + state:label(5,1,"info3", "") + + local preview = state:container(5,2,"craft_preview") + state:background(4.9, 0.1, 3, 3.8, "craft_img1", "minimap_overlay_square.png") + craft_preview(preview:getContainerState()) + state:item_image(7,2.5,0.8,0.8,"craft_result",nil):setIsHidden(true) + update_listview(state) + update_icons_list(state) +end + + +smart_inventory.register_page("crafting", { + icon = "inventory_btn.png", + smartfs_callback = crafting_callback +}) diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..d6ee3df --- /dev/null +++ b/depends.txt @@ -0,0 +1,3 @@ +default +creative? +sfinv? diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..e5b7a9b --- /dev/null +++ b/init.lua @@ -0,0 +1,84 @@ +local modpath = minetest.get_modpath(minetest.get_current_modname()) + +smart_inventory = {} +smart_inventory.registered_pages = {} +smart_inventory.smartfs = dofile(modpath.."/smartfs.lua") +smartfs = smart_inventory.smartfs + +local inventory_form = smartfs.create("smart_inventory:main", function(state) + -- tabbed view controller + local tab_controller = { + _tabs = {}, + active_name = nil, + set_active = function(self, tabname) + for name, def in pairs(self._tabs) do + if name == tabname then + def.button:setBackground("halo.png") + def.view:setIsHidden(false) + else + def.button:setBackground(nil) + def.view:setIsHidden(true) + end + end + self.active_name = tabname + end, + tab_add = function(self, name, def) + def.viewstate:size(12,8) --size of tab view + self._tabs[name] = def + end, + get_active_name = function(self) + return self.active_name + end, + } + + --set screen size + state:size(14,10) + state:label(1,0.2,"header","Smart Inventory") + state:image(0,0,1,1,"header_logo", "logo.png") + local button_x = 0.1 + for name, def in pairs(smart_inventory.registered_pages) do + assert(def.smartfs_callback, "Callback function needed") + local tabdef = {} + local label + if not def.label then + label = "" + else + label = def.label + end + tabdef.button = state:button(button_x,9.2,1,1,name.."_button",label) + if def.icon then + tabdef.button:setImage(def.icon) + end + tabdef.button:onClick(function(self) + tab_controller:set_active(name) + end) + tabdef.view = state:container(0,1,name.."_container") + tabdef.viewstate = tabdef.view:getContainerState() + tabdef.viewstate:loadTemplate(def.smartfs_callback) + tab_controller:tab_add(name, tabdef) + if button_x < 1 then + tab_controller:set_active(name) + end + button_x = button_x + 2 + end + +end) + +smartfs.set_player_inventory(inventory_form) + + +function smart_inventory.register_page(name, def) + smart_inventory.registered_pages[name] = def +end +--[[ +API: +smart_inventory.register_page(name, { + icon | label = + check_active = (optional: function to check if active) + smartfs_callback = +}) + +]] + + +dofile(modpath.."/crafting.lua") diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..fd9e50f --- /dev/null +++ b/mod.conf @@ -0,0 +1,4 @@ +name = smart_inventory +title = Smart Inventory +author = bell07 +license = CC0 diff --git a/smartfs.lua b/smartfs.lua new file mode 100644 index 0000000..e49a5d6 --- /dev/null +++ b/smartfs.lua @@ -0,0 +1,1348 @@ +--------------------------- +-- 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 unified_inventory then + return "unified_inventory" + elseif 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) + minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true)) + end + } + end +} + +-- Standalone inventory +smartfs._ldef.inventory = { + set_inventory = function(form) + if sfinv and sfinv.enabled then + sfinv.enabled = false + 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 + minetest.after(0, state.show, state) + 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) + local player = minetest.get_player_by_name(state.location.player) + player:set_inventory_formspec(state:_buildFormspec_(true)) + end + } + end +} + +-- Node metadata +smartfs._ldef.nodemeta = { + _make_state_location_ = function(nodepos) + return { + type = "nodemeta", + pos = nodepos, + _show_ = function(state) + local meta = minetest.get_meta(state.location.pos) + meta:set_string("formspec", state:_buildFormspec_(true)) + meta:set_string("smartfs_name", state.def.name) + 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, newplayer, params, is_inv, nodepos) +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 + + local compat_is_inv + if location.type == "inventory" then + compat_is_inv = true + else + compat_is_inv = false + end + + ------------------------------------------------------ + -- State - create returning state object + ------------------------------------------------------ + return { + _ele = {}, + def = form, + players = _make_players_(newplayer), + location = location, + is_inv = compat_is_inv, -- obsolete / compatibility + player = newplayer, -- obsolete / compatibility + 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 not val:getIsHidden() == true then + res = res .. val:build() + end + end + return res + end, + show = location._show_, + -- process /apply received field value + _sfs_process_value_ = function(self, field, value) -- process each single received field + local cur_namespace = self:getNamespace() + if cur_namespace == "" or cur_namespace == string.sub(field, 1, string.len(cur_namespace)) then -- Check current namespace + local rel_fieldname = string.sub(field, string.len(cur_namespace)+1) --cut the namespace + if self._ele[rel_fieldname] then -- direct top-level assignment + self._ele[rel_fieldname].data.value = value + else + for elename, eledef in pairs(self._ele) do + if eledef.getContainerState then -- element supports sub-states + eledef:getContainerState():_sfs_process_value_(field, value) + end + end + end + end + end, + -- process action for received field if supported + _sfs_process_action_ = function(self, field, value, player) + local cur_namespace = self:getNamespace() + if cur_namespace == "" or cur_namespace == string.sub(field, 1, string.len(cur_namespace)) then -- Check current namespace + local rel_fieldname = string.sub(field, string.len(cur_namespace)+1) --cut the namespace + if self._ele[rel_fieldname] then -- direct top-level assignment + if self._ele[rel_fieldname].submit then + self._ele[rel_fieldname]:submit(value, player) + end + else + for elename, eledef in pairs(self._ele) do + if eledef.getContainerState then -- element supports sub-states + eledef:getContainerState():_sfs_process_action_(field, value, player) + end + end + end + end + end, + -- process onInput hook for the state + _sfs_process_oninput_ = function(self, fields, player) --process hooks + -- call onInput hook if enabled + 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 not eledef:getIsHidden() 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) + -- fields assignment + for field, value in pairs(fields) do + self:_sfs_process_value_(field, value) + end + -- process onInput hooks + self:_sfs_process_oninput_(fields, player) + + -- do actions + for field, value in pairs(fields) do + self:_sfs_process_action_(field, value, player) + 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, + setIsHidden = function(self, hidden) + self.data.hidden = hidden + end, + getIsHidden = function(self) + return self.data.hidden + 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, + } + + 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, + loadTemplate = function(self, template) + -- template can be a function (usable in smartfs.create()), a form name or object ( a smartfs.create() result) + if type(template) == "function" then -- asume it is a smartfs.create() usable function + return template(self) + elseif type(template) == "string" then -- asume it is a form name + return smartfs.__call(self, template)._reg(self) + elseif type(template) == "table" then --asume it is an other state + if template._reg then + template._reg(self) + end + end + 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 + }) + 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, + value = 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) + return self:element("container", { + pos = {x=x, y=y}, + name = name + }) + 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).."]".. + self:getBackgroundString() + return specstring + end, + + submit = function(self, field, player) + if self._click then + self:_click(self.root, player) + end + --[[ not needed. there is a quit field received in this case + if self.data.closes then + self.root.location.rootState:close() + end + ]]-- + end, + onClick = function(self,func) + self._click = func + end, + click = function(self,func) + self._click = func + end, + setText = function(self,text) + self.data.value = 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 +}) + +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]).. + "]".. + self:getBackgroundString() + 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) + return "label[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + minetest.formspec_escape(self.data.value).. + "]".. + self:getBackgroundString() + end, + setText = function(self,text) + self.data.value = 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).. + "]".. + self:getBackgroundString() + 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:getBackgroundString() + 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:getBackgroundString() + end + end, + setText = function(self,text) + self.data.value = 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 +}) + +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 + self.data.background = self.data.value + return self: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) + self.data.value = text + end, + getImage = function(self) + return self.data.value + 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) .."]".. + self:getBackgroundString() + end, + submit = function(self, field, player) + -- self.data.value already set by value transfer, but as string + self.data.value = minetest.is_yes(field) + -- 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").."]" .. + self:getBackgroundString() + 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()).. + "]".. + self:getBackgroundString() + 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 "") .. + "]".. + self:getBackgroundString() + 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) + --the background string is "under" the container. Parts of this background can be overriden by elements (with background) from container + return self:getBackgroundString().. + "container["..self.data.pos.x..","..self.data.pos.y.."]".. + self:getContainerState():_buildFormspec_(false).. + "container_end[]" + end, + getContainerState = function(self) + return self._state + end + -- submit is handled by framework for elements with getContainerState +}) + +return smartfs