local S = minetest.get_translator("unified_inventory") local F = minetest.formspec_escape local ui = unified_inventory -- This pair of encoding functions is used where variable text must go in -- button names, where the text might contain formspec metacharacters. -- We can escape button names for the formspec, to avoid screwing up -- form structure overall, but they then don't get de-escaped, and so -- the input we get back from the button contains the formspec escaping. -- This is a game engine bug, and in the anticipation that it might be -- fixed some day we don't want to rely on it. So for safety we apply -- an encoding that avoids all formspec metacharacters. function ui.mangle_for_formspec(str) return string.gsub(str, "([^A-Za-z0-9])", function (c) return string.format("_%d_", string.byte(c)) end) end function ui.demangle_for_formspec(str) return string.gsub(str, "_([0-9]+)_", function (v) return string.char(v) end) end -- Get the player-specific unified_inventory style function ui.get_per_player_formspec(player_name) local draw_lite_mode = ui.lite_mode and not minetest.check_player_privs(player_name, {ui_full=true}) local style = table.copy(draw_lite_mode and ui.style_lite or ui.style_full) style.is_lite_mode = draw_lite_mode return style end -- Creates an item image or regular image button with a tooltip local function formspec_button(ui_peruser, name, image, offset, pos, scale, label) local element = 'image_button' if minetest.registered_items[image] then element = 'item_image_button' elseif image:find(":", 1, true) then image = "unknown_item.png" end local spc = (1-scale)*ui_peruser.btn_size/2 local size = ui_peruser.btn_size*scale return string.format("%s[%f,%f;%f,%f;%s;%s;]", element, (offset.x or offset[1]) + ( ui_peruser.btn_spc * (pos.x or pos[1]) ) + spc, (offset.y or offset[2]) + ( ui_peruser.btn_spc * (pos.y or pos[2]) ) + spc, size, size, image, name) .. string.format("tooltip[%s;%s]", name, F(label or name)) end -- Add registered buttons (tabs) local function formspec_tab_buttons(player, formspec, style) local n = #formspec + 1 -- Main buttons local filtered_inv_buttons = {} for i, def in pairs(ui.buttons) do if not (style.is_lite_mode and def.hide_lite) then table.insert(filtered_inv_buttons, def) end end local needs_scrollbar = #filtered_inv_buttons > style.main_button_cols * style.main_button_rows formspec[n] = ("scroll_container[%g,%g;%g,%g;tabbtnscroll;vertical]"):format( style.main_button_x, style.main_button_y, -- position style.main_button_cols * style.btn_spc, style.main_button_rows -- size ) n = n + 1 for i, def in pairs(filtered_inv_buttons) do local pos_x = ((i - 1) % style.main_button_cols) * style.btn_spc local pos_y = math.floor((i - 1) / style.main_button_cols) * style.btn_spc if def.type == "image" then if (def.condition == nil or def.condition(player) == true) then formspec[n] = string.format("image_button[%g,%g;%g,%g;%s;%s;]", pos_x, pos_y, style.btn_size, style.btn_size, F(def.image), F(def.name)) formspec[n+1] = "tooltip["..F(def.name)..";"..(def.tooltip or "").."]" n = n+2 else -- formspec[n] = string.format("image[%g,%g;%g,%g;%s^[colorize:#808080:alpha]", -- pos_x, pos_y, style.btn_size, style.btn_size, -- def.image) -- n = n+1 end end end formspec[n] = "scroll_container_end[]" if needs_scrollbar then formspec[n+1] = ("scrollbaroptions[max=%i;arrows=hide]"):format( -- This calculation is not 100% accurate but "good enough" math.ceil((#filtered_inv_buttons - 1) / style.main_button_cols) * style.btn_spc * 5 ) formspec[n+2] = ("scrollbar[%g,%g;0.4,%g;vertical;tabbtnscroll;0]"):format( style.main_button_x + style.main_button_cols * style.btn_spc - 0.1, -- x pos style.main_button_y, -- y pos style.main_button_rows * style.btn_spc -- height ) formspec[n+3] = "scrollbaroptions[max=1000;arrows=default]" end end local function formspec_add_search_box(player, formspec, ui_peruser) local player_name = player:get_player_name() local n = #formspec + 1 formspec[n] = "field_close_on_enter[searchbox;false]" formspec[n+1] = string.format("field[%f,%f;%f,%f;searchbox;;%s]", ui_peruser.page_buttons_x, ui_peruser.page_buttons_y, ui_peruser.searchwidth - 0.1, ui_peruser.btn_size, F(ui.current_searchbox[player_name])) formspec[n+2] = string.format("image_button[%f,%f;%f,%f;ui_search_icon.png;searchbutton;]", ui_peruser.page_buttons_x + ui_peruser.searchwidth, ui_peruser.page_buttons_y, ui_peruser.btn_size,ui_peruser.btn_size) formspec[n+3] = "tooltip[searchbutton;" ..F(S("Search")) .. "]" formspec[n+4] = string.format("image_button[%f,%f;%f,%f;ui_reset_icon.png;searchresetbutton;]", ui_peruser.page_buttons_x + ui_peruser.searchwidth + ui_peruser.btn_spc, ui_peruser.page_buttons_y, ui_peruser.btn_size, ui_peruser.btn_size) formspec[n+5] = "tooltip[searchresetbutton;"..F(S("Reset search and display everything")).."]" if ui.activefilter[player_name] ~= "" then formspec[n+6] = string.format("label[%f,%f;%s: %s]", ui_peruser.page_x, ui_peruser.page_y - 0.25, F(S("Filter")), F(ui.activefilter[player_name])) end end local function formspec_add_item_browser(player, formspec, ui_peruser) local player_name = player:get_player_name() local n = #formspec + 1 formspec[n] = string.format("image_button[%f,%f;%f,%f;ui_left_icon.png;rewind1;]", ui_peruser.page_buttons_x, ui_peruser.page_buttons_y + 1, ui_peruser.btn_size, ui_peruser.btn_size) formspec[n+1] = "tooltip[rewind1;"..F(S("Back one page")).."]" formspec[n+2] = string.format("image_button[%f,%f;%f,%f;ui_right_icon.png;forward1;]", ui_peruser.page_buttons_x + ui_peruser.btn_spc * 5, ui_peruser.page_buttons_y + 1, ui_peruser.btn_size, ui_peruser.btn_size) formspec[n+3] = "tooltip[forward1;"..F(S("Forward one page")).."]" n = n + 4 -- Items list if #ui.filtered_items_list[player_name] == 0 then local no_matches = S("No matching items") if ui_peruser.is_lite_mode then no_matches = S("No matches.") end formspec[n] = "label["..ui_peruser.page_x..","..(ui_peruser.page_y+0.15)..";" .. F(no_matches) .. "]" return end local dir = ui.active_search_direction[player_name] local list_index = ui.current_index[player_name] local page2 = math.floor(list_index / (ui_peruser.items_per_page) + 1) local pagemax = math.floor( (#ui.filtered_items_list[player_name] - 1) / (ui_peruser.items_per_page) + 1) for y = 0, ui_peruser.pagerows - 1 do for x = 0, ui_peruser.pagecols - 1 do local name = ui.filtered_items_list[player_name][list_index] local item = minetest.registered_items[name] if item then -- Clicked on current item: Flip crafting direction if name == ui.current_item[player_name] then local cdir = ui.current_craft_direction[player_name] if cdir == "recipe" then dir = "usage" elseif cdir == "usage" then dir = "recipe" end else -- Default: use active search direction by default dir = ui.active_search_direction[player_name] end local button_name = "item_button_" .. dir .. "_" .. ui.mangle_for_formspec(name) formspec[n] = ("item_image_button[%f,%f;%f,%f;%s;%s;]"):format( ui_peruser.page_x + x * ui_peruser.btn_spc, ui_peruser.page_y + y * ui_peruser.btn_spc, ui_peruser.btn_size, ui_peruser.btn_size, name, button_name ) local tooltip = item.description if item.mod_origin then -- "mod_origin" may not be specified for items that were -- registered in a callback (during or before ServerEnv init) tooltip = tooltip .. " [" .. item.mod_origin .. "]" end formspec[n + 1] = ("tooltip[%s;%s]"):format( button_name, minetest.formspec_escape(tooltip) ) n = n + 2 list_index = list_index + 1 end end end formspec[n] = string.format("label[%f,%f;%s: %s]", ui_peruser.page_buttons_x + ui_peruser.btn_spc * (ui_peruser.is_lite_mode and 1 or 2), ui_peruser.page_buttons_y - 0.3 + ui_peruser.btn_spc * 2, F(S("Page")), S("@1 of @2",page2,pagemax)) end function ui.get_formspec(player, page) if not player then return "" end local player_name = player:get_player_name() local ui_peruser = ui.get_per_player_formspec(player_name) ui.current_page[player_name] = page local pagedef = ui.pages[page] if not pagedef then return "" -- Invalid page name end local fs = { "formspec_version[4]", "size["..ui_peruser.formw..","..ui_peruser.formh.."]", pagedef.formspec_prepend and "" or "no_prepend[]", ui.standard_background } local perplayer_formspec = ui.get_per_player_formspec(player_name) local fsdata = pagedef.get_formspec(player, perplayer_formspec) fs[#fs + 1] = fsdata.formspec formspec_tab_buttons(player, fs, ui_peruser) if fsdata.draw_inventory ~= false then -- Player inventory fs[#fs + 1] = "listcolors[#00000000;#00000000]" fs[#fs + 1] = ui_peruser.standard_inv end if fsdata.draw_item_list == false then return table.concat(fs, "") end formspec_add_search_box(player, fs, ui_peruser) formspec_add_item_browser(player, fs, ui_peruser) return table.concat(fs) end function ui.set_inventory_formspec(player, page) if player then player:set_inventory_formspec(ui.get_formspec(player, page)) end end local function valid_def(def) return (not def.groups.not_in_creative_inventory or def.groups.not_in_creative_inventory == 0) and def.description and def.description ~= "" end --apply filter to the inventory list (create filtered copy of full one) function ui.apply_filter(player, filter, search_dir) if not player then return false end local player_name = player:get_player_name() local lfilter = string.lower(filter) local ffilter if lfilter:sub(1, 6) == "group:" then local groups = lfilter:sub(7):split(",") ffilter = function(name, def) for _, group in ipairs(groups) do if not def.groups[group] or def.groups[group] <= 0 then return false end end return true end else local player_info = minetest.get_player_information(player_name) local lang = player_info and player_info.lang_code or "" ffilter = function(name, def) local lname = string.lower(name) local ldesc = string.lower(def.description) local llocaldesc = minetest.get_translated_string and string.lower(minetest.get_translated_string(lang, def.description)) return string.find(lname, lfilter, 1, true) or string.find(ldesc, lfilter, 1, true) or llocaldesc and string.find(llocaldesc, lfilter, 1, true) end end ui.filtered_items_list[player_name]={} for name, def in pairs(minetest.registered_items) do if valid_def(def) and ffilter(name, def) then table.insert(ui.filtered_items_list[player_name], name) end end table.sort(ui.filtered_items_list[player_name]) ui.filtered_items_list_size[player_name] = #ui.filtered_items_list[player_name] ui.current_index[player_name] = 1 ui.activefilter[player_name] = filter ui.active_search_direction[player_name] = search_dir ui.set_inventory_formspec(player, ui.current_page[player_name]) end -- Inform players about potential visual issues minetest.register_on_joinplayer(function(player) local player_name = player:get_player_name() local info = minetest.get_player_information(player_name) if info and (info.formspec_version or 0) < 4 then minetest.chat_send_player(player_name, S("Unified Inventory: Your game version is too old" .. " and does not support the GUI requirements. You might experience visual issues.")) end end)