From 23a45b81318006b204406e628a8de482800eee23 Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Sun, 23 Jan 2022 18:15:17 +0100 Subject: [PATCH 1/5] Only update formspec on search filter changes --- callbacks.lua | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/callbacks.lua b/callbacks.lua index 5874a0f..d82e93f 100644 --- a/callbacks.lua +++ b/callbacks.lua @@ -1,3 +1,5 @@ +local ui = unified_inventory + local function default_refill(stack) stack:set_count(stack:get_stack_max()) local itemdef = minetest.registered_items[stack:get_name()] @@ -48,11 +50,11 @@ end) local function apply_new_filter(player, search_text, new_dir) local player_name = player:get_player_name() + minetest.sound_play("click", {to_player=player_name, gain = 0.1}) - unified_inventory.apply_filter(player, search_text, new_dir) - unified_inventory.current_searchbox[player_name] = search_text - unified_inventory.set_inventory_formspec(player, - unified_inventory.current_page[player_name]) + ui.apply_filter(player, search_text, new_dir) + ui.current_searchbox[player_name] = search_text + ui.set_inventory_formspec(player, ui.current_page[player_name]) end minetest.register_on_player_receive_fields(function(player, formname, fields) @@ -65,9 +67,12 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) end -- always take new search text, even if not searching on it yet + local dirty_search_filter = false + if fields.searchbox and fields.searchbox ~= unified_inventory.current_searchbox[player_name] then unified_inventory.current_searchbox[player_name] = fields.searchbox + dirty_search_filter = true end @@ -88,19 +93,14 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) unified_inventory.current_page[player_name]) end - if fields.next_category then - local scroll = math.min(#unified_inventory.category_list-ui_peruser.pagecols, unified_inventory.current_category_scroll[player_name] + 1) - if scroll ~= unified_inventory.current_category_scroll[player_name] then - unified_inventory.current_category_scroll[player_name] = scroll - unified_inventory.set_inventory_formspec(player, - unified_inventory.current_page[player_name]) - end - end - if fields.prev_category then - local scroll = math.max(0, unified_inventory.current_category_scroll[player_name] - 1) - if scroll ~= unified_inventory.current_category_scroll[player_name] then - unified_inventory.current_category_scroll[player_name] = scroll - unified_inventory.set_inventory_formspec(player, + if fields.next_category or fields.prev_category then + local step = fields.next_category and 1 or -1 + local scroll_old = ui.current_category_scroll[player_name] + local scroll_new = math.max(0, math.min(#ui.category_list - ui_peruser.pagecols, scroll_old + step)) + + if scroll_old ~= scroll_new then + ui.current_category_scroll[player_name] = scroll_new + ui.set_inventory_formspec(player, unified_inventory.current_page[player_name]) end end @@ -197,13 +197,16 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) if fields.searchbutton or fields.key_enter_field == "searchbox" then - unified_inventory.apply_filter(player, unified_inventory.current_searchbox[player_name], "nochange") - unified_inventory.set_inventory_formspec(player, - unified_inventory.current_page[player_name]) - minetest.sound_play("paperflip2", - {to_player=player_name, gain = 1.0}) + if dirty_search_filter then + ui.apply_filter(player, ui.current_searchbox[player_name], "nochange") + ui.set_inventory_formspec(player, ui.current_page[player_name]) + minetest.sound_play("paperflip2", + {to_player=player_name, gain = 1.0}) + end elseif fields.searchresetbutton then - apply_new_filter(player, "", "nochange") + if ui.current_searchbox[player_name] ~= "" then + apply_new_filter(player, "", "nochange") + end end -- alternate buttons From 25c40fea6cb9c1d563137d3d9088982b073a4435 Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Mon, 24 Jan 2022 15:20:57 +0100 Subject: [PATCH 2/5] Fix filter reset for empty search boxes --- callbacks.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/callbacks.lua b/callbacks.lua index d82e93f..af2c127 100644 --- a/callbacks.lua +++ b/callbacks.lua @@ -204,7 +204,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) {to_player=player_name, gain = 1.0}) end elseif fields.searchresetbutton then - if ui.current_searchbox[player_name] ~= "" then + if ui.activefilter[player_name] ~= "" then apply_new_filter(player, "", "nochange") end end From 5ac2558da441b25c19fb6ecbe1cfe427c44663af Mon Sep 17 00:00:00 2001 From: James William Fletcher <78346668+mrbid@users.noreply.github.com> Date: Wed, 23 Feb 2022 20:48:26 +0000 Subject: [PATCH 3/5] Speed up group filtering code This improves the startup time dramatically by lowering the algorithm complexity. Mainly noticeable with large inventory sizes. Written by Test_User, post-edited by SmallJoker --- api.lua | 56 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/api.lua b/api.lua index 8cc5532..64efe0b 100644 --- a/api.lua +++ b/api.lua @@ -43,7 +43,7 @@ minetest.after(0.01, function() end table.sort(ui.items_list) ui.items_list_size = #ui.items_list - print("Unified Inventory. inventory size: "..ui.items_list_size) + print("Unified Inventory. Inventory size: "..ui.items_list_size) for _, name in ipairs(ui.items_list) do local def = minetest.registered_items[name] -- Simple drops @@ -133,19 +133,61 @@ minetest.after(0.01, function() end end end + + -- Step 1: group-indexed lookup table for items + local spec_matcher = {} + for _, name in ipairs(ui.items_list) do + -- we only need to care about groups, exact items are handled separately + for group, value in pairs(minetest.registered_items[name].groups) do + if value and value ~= 0 then + if not spec_matcher[group] then + spec_matcher[group] = {} + end + spec_matcher[group][name] = true + end + end + end + + -- Step 2: Find all matching items for the given spec (groups) + local function get_matching_spec_items(specname) + if specname:sub(1,6) ~= "group:" then + return { specname } + end + + local accepted = {} + for i, group in ipairs(specname:sub(7):split(",")) do + if i == 1 then + -- First step: Copy all possible item names in this group + for name, _ in pairs(spec_matcher[group] or {}) do + accepted[name] = true + end + else + -- Perform filtering + if spec_matcher[group] then + for name, _ in pairs(accepted) do + accepted[name] = spec_matcher[group][name] + end + else + -- No matching items + return {} + end + end + end + return accepted + end + for _, recipes in pairs(ui.crafts_for.recipe) do for _, recipe in ipairs(recipes) do local ingredient_items = {} for _, spec in pairs(recipe.items) do - local matches_spec = ui.canonical_item_spec_matcher(spec) - for _, name in ipairs(ui.items_list) do - if matches_spec(name) then - ingredient_items[name] = true - end + -- Get items that fit into this spec (group or item name) + local specname = ItemStack(spec):get_name() + for item_name, _ in pairs(get_matching_spec_items(specname)) do + ingredient_items[item_name] = true end end for name, _ in pairs(ingredient_items) do - if ui.crafts_for.usage[name] == nil then + if not ui.crafts_for.usage[name] then ui.crafts_for.usage[name] = {} end table.insert(ui.crafts_for.usage[name], recipe) From 3074d625e2797765d72cc2456384f78212937168 Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Sat, 26 Feb 2022 11:36:56 +0100 Subject: [PATCH 4/5] Version 4: Feature checks and deprecate internal functions --- callbacks.lua | 17 +++++++--------- group.lua | 41 -------------------------------------- init.lua | 9 ++++++++- internal.lua | 10 ++++++++++ legacy.lua | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 52 deletions(-) create mode 100644 legacy.lua diff --git a/callbacks.lua b/callbacks.lua index af2c127..e92175d 100644 --- a/callbacks.lua +++ b/callbacks.lua @@ -14,9 +14,9 @@ end minetest.register_on_joinplayer(function(player) local player_name = player:get_player_name() unified_inventory.players[player_name] = {} - unified_inventory.current_index[player_name] = 1 + unified_inventory.current_index[player_name] = 1 -- Item (~page) index unified_inventory.filtered_items_list[player_name] = - unified_inventory.items_list + unified_inventory.items_list unified_inventory.activefilter[player_name] = "" unified_inventory.active_search_direction[player_name] = "nochange" unified_inventory.apply_filter(player, "", "nochange") @@ -244,11 +244,8 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) unified_inventory.current_page[player_name]) end) -if minetest.delete_detached_inventory then - minetest.register_on_leaveplayer(function(player) - local player_name = player:get_player_name() - minetest.delete_detached_inventory(player_name.."_bags") - minetest.delete_detached_inventory(player_name.."craftrecipe") - minetest.delete_detached_inventory(player_name.."refill") - end) -end +minetest.register_on_leaveplayer(function(player) + local player_name = player:get_player_name() + minetest.remove_detached_inventory(player_name.."_bags") + minetest.remove_detached_inventory(player_name.."refill") +end) diff --git a/group.lua b/group.lua index 3864267..2bc8e2f 100644 --- a/group.lua +++ b/group.lua @@ -1,30 +1,5 @@ local S = minetest.get_translator("unified_inventory") -function unified_inventory.canonical_item_spec_matcher(spec) - local specname = ItemStack(spec):get_name() - if specname:sub(1, 6) ~= "group:" then - return function (itemname) - return itemname == specname - end - end - - local group_names = specname:sub(7):split(",") - return function (itemname) - local itemdef = minetest.registered_items[itemname] - for _, group_name in ipairs(group_names) do - if (itemdef.groups[group_name] or 0) == 0 then - return false - end - end - return true - end -end - -function unified_inventory.item_matches_spec(item, spec) - local itemname = ItemStack(item):get_name() - return unified_inventory.canonical_item_spec_matcher(spec)(itemname) -end - function unified_inventory.extract_groupnames(groupname) local specname = ItemStack(groupname):get_name() if specname:sub(1, 6) ~= "group:" then @@ -34,22 +9,6 @@ function unified_inventory.extract_groupnames(groupname) return table.concat(group_names, S(" and ")), #group_names end -unified_inventory.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", -} - -function unified_inventory.register_group_item(groupname, itemname) - unified_inventory.registered_group_items[groupname] = itemname -end - -- This is used when displaying craft recipes, where an ingredient is -- specified by group rather than as a specific item. A single-item group diff --git a/init.lua b/init.lua index 98ee2e5..38cc8cb 100644 --- a/init.lua +++ b/init.lua @@ -1,5 +1,11 @@ -- Unified Inventory +if not minetest.features.formspec_version_element then + -- At least formspec_version[] is the minimal feature requirement + error("Unified Inventory requires Minetest version 5.4.0 or newer.\n" .. + " Please update Minetest or use an older version of Unified Inventory.") +end + local modpath = minetest.get_modpath(minetest.get_current_modname()) local worldpath = minetest.get_worldpath() @@ -46,7 +52,7 @@ unified_inventory = { list_img_offset = 0.13, standard_background = "background9[0,0;1,1;ui_formbg_9_sliced.png;true;16]", - version = 3 + version = 4 } local ui = unified_inventory @@ -170,3 +176,4 @@ end dofile(modpath.."/item_names.lua") dofile(modpath.."/waypoints.lua") +dofile(modpath.."/legacy.lua") -- mod compatibility diff --git a/internal.lua b/internal.lua index b49906e..c43bd32 100644 --- a/internal.lua +++ b/internal.lua @@ -378,3 +378,13 @@ function ui.apply_filter(player, filter, search_dir) 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) diff --git a/legacy.lua b/legacy.lua new file mode 100644 index 0000000..5b62894 --- /dev/null +++ b/legacy.lua @@ -0,0 +1,55 @@ +-- Inefficient pattern matching + +local warned_funcs = {} +local function LOG_ONCE(funcname) + if warned_funcs[funcname] then return end + warned_funcs[funcname] = true + minetest.log("error", "Call to undocumented, deprecated API '" .. funcname .. "'." + .. " In a future version of Unified Inventory this will result in a real error.") +end + +function unified_inventory.canonical_item_spec_matcher(spec) + LOG_ONCE("canonical_item_spec_matcher") + local specname = ItemStack(spec):get_name() + if specname:sub(1, 6) ~= "group:" then + return function (itemname) + return itemname == specname + end + end + + local group_names = specname:sub(7):split(",") + return function (itemname) + local itemdef = minetest.registered_items[itemname] + for _, group_name in ipairs(group_names) do + if (itemdef.groups[group_name] or 0) == 0 then + return false + end + end + return true + end +end + +function unified_inventory.item_matches_spec(item, spec) + LOG_ONCE("item_matches_spec") + local itemname = ItemStack(item):get_name() + return unified_inventory.canonical_item_spec_matcher(spec)(itemname) +end + + +unified_inventory.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", +} + +function unified_inventory.register_group_item(groupname, itemname) + LOG_ONCE("register_group_item") + unified_inventory.registered_group_items[groupname] = itemname +end + From dbe06be68b72e6ae2e3c98b610a7fd6610d35b3e Mon Sep 17 00:00:00 2001 From: SmallJoker Date: Sat, 26 Feb 2022 11:41:15 +0100 Subject: [PATCH 5/5] Add scroll bar for overflowing tab buttons (#195) --- init.lua | 28 ++++++++++++++++++++++------ internal.lua | 44 +++++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/init.lua b/init.lua index 38cc8cb..93e45c4 100644 --- a/init.lua +++ b/init.lua @@ -65,10 +65,16 @@ ui.style_full = { formspec_y = 1, formw = 17.75, formh = 12.25, + -- Item browser size, pos pagecols = 8, pagerows = 9, page_x = 10.75, page_y = 2.30, + -- Item browser controls + page_buttons_x = 11.60, + page_buttons_y = 10.15, + searchwidth = 3.4, + -- Crafting grid positions craft_x = 2.8, craft_y = 1.15, craftresult_x = 7.8, @@ -80,13 +86,15 @@ ui.style_full = { craft_guide_resultstr_x = 0.3, craft_guide_resultstr_y = 0.6, give_btn_x = 0.25, + -- Tab switching buttons main_button_x = 0.4, main_button_y = 11.0, - page_buttons_x = 11.60, - page_buttons_y = 10.15, - searchwidth = 3.4, + main_button_cols = 12, + main_button_rows = 1, + -- Tab title position form_header_x = 0.4, form_header_y = 0.4, + -- Generic sizes btn_spc = 0.85, btn_size = 0.75, std_inv_x = 0.3, @@ -98,10 +106,16 @@ ui.style_lite = { formspec_y = 0.6, formw = 14, formh = 9.75, + -- Item browser size, pos pagecols = 4, pagerows = 5, page_x = 10.5, page_y = 2.15, + -- Item browser controls + page_buttons_x = 10.5, + page_buttons_y = 6.15, + searchwidth = 1.6, + -- Crafting grid positions craft_x = 2.6, craft_y = 0.75, craftresult_x = 5.75, @@ -113,13 +127,15 @@ ui.style_lite = { craft_guide_resultstr_x = 0.15, craft_guide_resultstr_y = 0.35, give_btn_x = 0.15, + -- Tab switching buttons main_button_x = 10.5, main_button_y = 8.15, - page_buttons_x = 10.5, - page_buttons_y = 6.15, - searchwidth = 1.6, + main_button_cols = 4, + main_button_rows = 2, + -- Tab title position form_header_x = 0.2, form_header_y = 0.2, + -- Generic sizes btn_spc = 0.8, btn_size = 0.7, std_inv_x = 0.1, diff --git a/internal.lua b/internal.lua index c43bd32..b264d22 100644 --- a/internal.lua +++ b/internal.lua @@ -18,7 +18,7 @@ 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}) @@ -27,6 +27,7 @@ function ui.get_per_player_formspec(player_name) 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 @@ -43,9 +44,8 @@ local function formspec_button(ui_peruser, name, image, offset, pos, scale, labe string.format("tooltip[%s;%s]", name, F(label or name)) end -local function formspec_add_filters(player, formspec, style) - local button_row = 0 - local button_col = 0 +-- Add registered buttons (tabs) +local function formspec_tab_buttons(player, formspec, style) local n = #formspec + 1 -- Main buttons @@ -58,32 +58,50 @@ local function formspec_add_filters(player, formspec, style) 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 - if style.is_lite_mode and i > 4 then - button_row = 1 - button_col = 1 - end + 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 - local pos_x = style.main_button_x + style.btn_spc * (i - 1) - button_col * style.btn_spc * 4 - local pos_y = style.main_button_y + button_row * style.btn_spc if (def.condition == nil or def.condition(player) == true) then - formspec[n] = string.format("image_button[%f,%f;%f,%f;%s;%s;]", + 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[%f,%f;%f,%f;%s^[colorize:#808080:alpha]", + 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 +-- Add category GUI elements (top right) local function formspec_add_categories(player, formspec, ui_peruser) local player_name = player:get_player_name() local n = #formspec + 1 @@ -280,7 +298,7 @@ function ui.get_formspec(player, page) fs[#fs + 1] = fsdata.formspec - formspec_add_filters(player, fs, ui_peruser) + formspec_tab_buttons(player, fs, ui_peruser) if fsdata.draw_inventory ~= false then -- Player inventory