diff --git a/README.md b/README.md index 5d23f79..e71fb42 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ From http://www.clker.com (Public Domain, CC-BY-4.0): * [`ui_pencil_icon.pnc`](http://www.clker.com/clipart-2256.html) * [`ui_waypoint_set_icon.png`](http://www.clker.com/clipart-larger-flag.html) +From https://www.svgrepo.com (CC-BY) + * [`ui_teleport.png`](https://www.svgrepo.com/svg/321565/teleport) + Everaldo Coelho (YellowIcon) (LGPL v2.1+): * [`ui_craftguide_icon.png` / `ui_craft_icon.png`](http://commons.wikimedia.org/wiki/File:Advancedsettings.png) @@ -102,3 +105,16 @@ Other files from Wikimedia Commons: RealBadAngel: (CC-BY-4.0) * Everything else. + + +## Sounds + + * [`bell.ogg`](https://freesound.org/people/bennstir/sounds/81072/) by bennstir, CC 4.0 + * [`electricity.ogg`](https://freesound.org/people/Halleck/sounds/19486/) by Halleck, CC 4.0 (cut) + * [`pageflip1.ogg`](https://freesound.org/people/themfish/sounds/45823/) by themfish, CC 4.0 (cut, slowed down) + * `pageflip2.ogg` (derived from `pageflip1.ogg`) + * [`trash.ogg`](https://freesound.org/people/OwlStorm/sounds/151231/) by OwlStorm, CC 0 (speed up) + * [`trash_all.ogg`](https://freesound.org/people/abel_K/sounds/68280/) by abel_K, Sampling Plus 1.0 (speed up) + * [`ui_click.ogg`](https://freesound.org/people/lartti/sounds/527569/) by lartti, CC 0 (cut) + * [`ui_morning.ogg`](https://freesound.org/people/InspectorJ/sounds/439472/) by InspectorJ, CC 4.0 + * [`ui_owl.ogg`](https://freesound.org/people/manda_g/sounds/54987/) by manda_g, Sampling Plus 1.0 (cut) diff --git a/api.lua b/api.lua index 1609217..0437391 100644 --- a/api.lua +++ b/api.lua @@ -2,40 +2,49 @@ local S = minetest.get_translator("unified_inventory") local F = minetest.formspec_escape local ui = unified_inventory +local function is_recipe_craftable(recipe) + -- Ensure the ingedients exist + for _, itemname in pairs(recipe.items) do + local groups = string.find(itemname, "group:") + if groups then + if not ui.get_group_item(string.sub(groups, 8)).item then + return false + end + else + -- Possibly an item + if not minetest.registered_items[itemname] + or minetest.get_item_group(itemname, "not_in_craft_guide") ~= 0 then + return false + end + end + end + return true +end + -- Create detached creative inventory after loading all mods minetest.after(0.01, function() local rev_aliases = {} - for source, target in pairs(minetest.registered_aliases) do - if not rev_aliases[target] then rev_aliases[target] = {} end - table.insert(rev_aliases[target], source) + for original, newname in pairs(minetest.registered_aliases) do + if not rev_aliases[newname] then + rev_aliases[newname] = {} + end + table.insert(rev_aliases[newname], original) end + + -- Filtered item list ui.items_list = {} for name, def in pairs(minetest.registered_items) do - if (not def.groups.not_in_creative_inventory or - def.groups.not_in_creative_inventory == 0) and - def.description and def.description ~= "" then + if ui.is_itemdef_listable(def) then table.insert(ui.items_list, name) + + -- Alias processing: Find recipes that belong to the current item name local all_names = rev_aliases[name] or {} table.insert(all_names, name) - for _, player_name in ipairs(all_names) do - local recipes = minetest.get_all_craft_recipes(player_name) - if recipes then - for _, recipe in ipairs(recipes) do - - local unknowns - - for _,chk in pairs(recipe.items) do - local groupchk = string.find(chk, "group:") - if (not groupchk and not minetest.registered_items[chk]) - or (groupchk and not ui.get_group_item(string.gsub(chk, "group:", "")).item) - or minetest.get_item_group(chk, "not_in_craft_guide") ~= 0 then - unknowns = true - end - end - - if not unknowns then - ui.register_craft(recipe) - end + for _, itemname in ipairs(all_names) do + local recipes = minetest.get_all_craft_recipes(itemname) + for _, recipe in ipairs(recipes or {}) do + if is_recipe_craftable(recipe) then + ui.register_craft(recipe) end end end @@ -44,6 +53,8 @@ minetest.after(0.01, function() table.sort(ui.items_list) ui.items_list_size = #ui.items_list print("Unified Inventory. Inventory size: "..ui.items_list_size) + + -- Analyse dropped items -> custom "digging" recipes for _, name in ipairs(ui.items_list) do local def = minetest.registered_items[name] -- Simple drops @@ -134,50 +145,18 @@ minetest.after(0.01, function() 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 1: Initialize cache for looking up groups + unified_inventory.init_matching_cache() -- 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] = true } - end + local get_matching_spec_items = unified_inventory.get_matching_items - 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 outputitemname, recipes in pairs(ui.crafts_for.recipe) do -- List of crafts that return this item string (variable "_") + + -- Problem: The group cache must be initialized after all mods finished loading + -- thus, invalid recipes might be indexed. Hence perform filtering with `new_recipe_list` + local new_recipe_list = {} for _, recipe in ipairs(recipes) do local ingredient_items = {} for _, spec in pairs(recipe.items) do @@ -193,7 +172,14 @@ minetest.after(0.01, function() end table.insert(ui.crafts_for.usage[name], recipe) end + + if next(ingredient_items) then + -- There's at least one known ingredient: mark as good recipe + -- PS: What whatll be done about partially incomplete recipes? + table.insert(new_recipe_list, recipe) + end end + ui.crafts_for.recipe[outputitemname] = new_recipe_list end for _, callback in ipairs(ui.initialized_callbacks) do @@ -201,8 +187,8 @@ minetest.after(0.01, function() end end) +---------------- Home API ---------------- --- load_home local function load_home() local input = io.open(ui.home_filename, "r") if not input then @@ -219,6 +205,7 @@ local function load_home() end io.close(input) end + load_home() function ui.set_home(player, pos) @@ -247,7 +234,8 @@ function ui.go_home(player) return false end --- register_craft +---------------- Crafting API ---------------- + function ui.register_craft(options) if not options.output then return @@ -270,14 +258,12 @@ function ui.register_craft(options) end end - local craft_type_defaults = { width = 3, height = 3, uses_crafting_grid = false, } - function ui.craft_type_defaults(name, options) if not options.description then options.description = name @@ -288,8 +274,7 @@ end function ui.register_craft_type(name, options) - ui.registered_craft_types[name] = - ui.craft_type_defaults(name, options) + ui.registered_craft_types[name] = ui.craft_type_defaults(name, options) end @@ -346,6 +331,8 @@ ui.register_craft_type("digging_chance", { height = 1, }) +---------------- GUI registrations ---------------- + function ui.register_page(name, def) ui.pages[name] = def end @@ -361,6 +348,8 @@ function ui.register_button(name, def) table.insert(ui.buttons, def) end +---------------- Callback registrations ---------------- + function ui.register_on_initialized(callback) if type(callback) ~= "function" then error(("Initialized callback must be a function, %s given."):format(type(callback))) @@ -375,6 +364,8 @@ function ui.register_on_craft_registered(callback) table.insert(ui.craft_registered_callbacks, callback) end +---------------- List getters ---------------- + function ui.get_recipe_list(output) return ui.crafts_for.recipe[output] end @@ -387,11 +378,15 @@ function ui.get_registered_outputs() return outputs end +---------------- Player utilities ---------------- + function ui.is_creative(playername) return minetest.check_player_privs(playername, {creative=true}) or minetest.settings:get_bool("creative_mode") end +---------------- Formspec helpers ---------------- + function ui.single_slot(xpos, ypos, bright) return string.format("background9[%f,%f;%f,%f;ui_single_slot%s.png;false;16]", xpos, ypos, ui.imgscale, ui.imgscale, (bright and "_bright" or "") ) diff --git a/bags.lua b/bags.lua index 14ac875..464b65b 100644 --- a/bags.lua +++ b/bags.lua @@ -10,25 +10,26 @@ local F = minetest.formspec_escape local ui = unified_inventory ui.register_page("bags", { - get_formspec = function(player) + get_formspec = function(player, perplayer_formspec) local player_name = player:get_player_name() - return { formspec = table.concat({ - ui.style_full.standard_inv_bg, - ui.single_slot(0.925, 1.5), - ui.single_slot(3.425, 1.5), - ui.single_slot(5.925, 1.5), - ui.single_slot(8.425, 1.5), - "label["..ui.style_full.form_header_x..","..ui.style_full.form_header_y..";" .. F(S("Bags")) .. "]", - "button[0.6125,2.75;1.875,0.75;bag1;" .. F(S("Bag @1", 1)) .. "]", - "button[3.1125,2.75;1.875,0.75;bag2;" .. F(S("Bag @1", 2)) .. "]", - "button[5.6125,2.75;1.875,0.75;bag3;" .. F(S("Bag @1", 3)) .. "]", - "button[8.1125,2.75;1.875,0.75;bag4;" .. F(S("Bag @1", 4)) .. "]", + local std_inv_x = perplayer_formspec.std_inv_x + local formspec = { + perplayer_formspec.standard_inv_bg, + "label[", perplayer_formspec.form_header_x, ",", + perplayer_formspec.form_header_y, ";", F(S("Bags")), "]", "listcolors[#00000000;#00000000]", - "list[detached:" .. F(player_name) .. "_bags;bag1;1.075,1.65;1,1;]", - "list[detached:" .. F(player_name) .. "_bags;bag2;3.575,1.65;1,1;]", - "list[detached:" .. F(player_name) .. "_bags;bag3;6.075,1.65;1,1;]", - "list[detached:" .. F(player_name) .. "_bags;bag4;8.575,1.65;1,1;]" - }) } + } + + for i = 1, 4 do + local x = std_inv_x + i * 2.5 + formspec[#formspec + 1] = ui.single_slot(x - 1.875, 1.5) + formspec[#formspec + 1] = string.format("list[detached:%s_bags;bag%i;%.3f,1.65;1,1;]", + F(player_name), i, x - 1.725) + formspec[#formspec + 1] = string.format("button[%.4f,2.75;1.875,0.75;bag%i;%s]", + x - 2.1875, i, F(S("Bag @1", i))) + end + + return { formspec = table.concat(formspec) } end, }) @@ -36,7 +37,6 @@ ui.register_button("bags", { type = "image", image = "ui_bags_icon.png", tooltip = S("Bags"), - hide_lite=true }) local function get_player_bag_stack(player, i) @@ -48,23 +48,38 @@ end for bag_i = 1, 4 do ui.register_page("bag" .. bag_i, { - get_formspec = function(player) + get_formspec = function(player, perplayer_formspec) local stack = get_player_bag_stack(player, bag_i) local image = stack:get_definition().inventory_image local slots = stack:get_definition().groups.bagslots + local std_inv_x = perplayer_formspec.std_inv_x + local lite_mode = perplayer_formspec.is_lite_mode + + local bag_inv_y, header_x, header_y = 1.5, 0.3, 0.65 + if lite_mode then + bag_inv_y = 0.5 + header_x = perplayer_formspec.form_header_x + header_y = perplayer_formspec.form_header_y + end local formspec = { - ui.style_full.standard_inv_bg, - ui.make_inv_img_grid(0.3, 1.5, 8, slots/8), - "image[9.2,0.4;1,1;" .. image .. "]", - "label[0.3,0.65;" .. F(S("Bag @1", bag_i)) .. "]", + perplayer_formspec.standard_inv_bg, + ui.make_inv_img_grid(std_inv_x, bag_inv_y, 8, slots/8), + "label[", header_x, ",", header_y, ";", F(S("Bag @1", bag_i)), "]", "listcolors[#00000000;#00000000]", "listring[current_player;main]", string.format("list[current_player;bag%icontents;%f,%f;8,3;]", - bag_i, 0.3 + ui.list_img_offset, 1.5 + ui.list_img_offset), - "listring[current_name;bag" .. bag_i .. "contents]", + bag_i, std_inv_x + ui.list_img_offset, bag_inv_y + ui.list_img_offset), + "listring[current_name;bag", bag_i, "contents]", } + + if lite_mode then + return { formspec = table.concat(formspec) } + end + local n = #formspec + 1 + formspec[n] = "image[" .. std_inv_x + 8.9 .. ",0.4;1,1;" .. image .. "]" + n = n + 1 local player_name = player:get_player_name() -- For if statement. if ui.trash_enabled @@ -114,6 +129,8 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) end end) +-- Player slots are preserved when unified_inventory is disabled. Do not allow modification. +-- Fix: use a detached inventory and store the data separately. local function save_bags_metadata(player, bags_inv) local is_empty = true local bags = {} @@ -163,7 +180,7 @@ local function load_bags_metadata(player, bags_inv) save_bags_metadata(player, bags_inv) end - -- Clean up deprecated garbage after saving + -- Legacy: Clean up old player lists for i = 1, 4 do local bag = "bag" .. i player_inv:set_size(bag, 0) @@ -172,46 +189,29 @@ end minetest.register_on_joinplayer(function(player) local player_name = player:get_player_name() - local bags_inv = minetest.create_detached_inventory(player_name .. "_bags",{ + local bags_inv = minetest.create_detached_inventory(player_name .. "_bags", { + allow_put = function(inv, listname, index, stack, player) + local new_slots = stack:get_definition().groups.bagslots + if not new_slots then + return 0 -- ItemStack is not a bag. + end + + -- The execution order of `allow_put`/`allow_take` is not defined. + -- We do not know the replacement ItemStack if the items are swapped. + -- Hence, bag slot upgrades and downgrades are not possible with the + -- current API. + + if not player:get_inventory():is_empty(listname .. "contents") then + -- Legacy: in case `allow_take` is not executed on old Minetest versions. + return 0 + end + return 1 + end, on_put = function(inv, listname, index, stack, player) player:get_inventory():set_size(listname .. "contents", stack:get_definition().groups.bagslots) save_bags_metadata(player, inv) end, - allow_put = function(inv, listname, index, stack, player) - local new_slots = stack:get_definition().groups.bagslots - if not new_slots then - return 0 - end - local player_inv = player:get_inventory() - local old_slots = player_inv:get_size(listname .. "contents") - - if new_slots >= old_slots then - return 1 - end - - -- using a smaller bag, make sure it fits - local old_list = player_inv:get_list(listname .. "contents") - local new_list = {} - local slots_used = 0 - local use_new_list = false - - for i, v in ipairs(old_list) do - if v and not v:is_empty() then - slots_used = slots_used + 1 - use_new_list = i > new_slots - new_list[slots_used] = v - end - end - if new_slots >= slots_used then - if use_new_list then - player_inv:set_list(listname .. "contents", new_list) - end - return 1 - end - -- New bag is smaller: Disallow inserting - return 0 - end, allow_take = function(inv, listname, index, stack, player) if player:get_inventory():is_empty(listname .. "contents") then return stack:get_count() @@ -230,6 +230,20 @@ minetest.register_on_joinplayer(function(player) load_bags_metadata(player, bags_inv) end) + +minetest.register_allow_player_inventory_action(function(player, action, inventory, info) + -- From detached inventory -> player inventory: put & take callbacks + if action ~= "put" or not info.listname:find("bag%dcontents") then + return + end + if info.stack:get_definition().groups.bagslots then + -- Problem 1: empty bags could be moved into their own slots + -- Problem 2: cannot reliably keep track of ItemStack ownership due to + --> Disallow all external bag movements into this list + return 0 + end +end) + -- register bag tools minetest.register_tool("unified_inventory:bag_small", { description = S("Small Bag"), diff --git a/callbacks.lua b/callbacks.lua index fa6d03a..52cb710 100644 --- a/callbacks.lua +++ b/callbacks.lua @@ -57,30 +57,47 @@ 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}) + minetest.sound_play("ui_click", {to_player=player_name, gain = 0.1}) 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) +-- Search box handling +local function receive_fields_searchbox(player, formname, fields) local player_name = player:get_player_name() - local ui_peruser,draw_lite_mode = unified_inventory.get_per_player_formspec(player_name) + -- always take new search text, even if not searching on it yet + if fields.searchbox and fields.searchbox ~= ui.current_searchbox[player_name] then + ui.current_searchbox[player_name] = fields.searchbox + end + if fields.searchbutton + or fields.key_enter_field == "searchbox" then + + if ui.current_searchbox[player_name] ~= ui.activefilter[player_name] 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 + if ui.activefilter[player_name] ~= "" then + apply_new_filter(player, "", "nochange") + end + end +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) if formname ~= "" then return end - -- always take new search text, even if not searching on it yet - local dirty_search_filter = false + receive_fields_searchbox(player, formname, fields) - 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 + local player_name = player:get_player_name() + local ui_peruser,draw_lite_mode = unified_inventory.get_per_player_formspec(player_name) local clicked_category for name, value in pairs(fields) do @@ -114,7 +131,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) for i, def in pairs(unified_inventory.buttons) do if fields[def.name] then def.action(player) - minetest.sound_play("click", + minetest.sound_play("ui_click", {to_player=player_name, gain = 0.1}) return end @@ -179,7 +196,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) end end if clicked_item then - minetest.sound_play("click", + minetest.sound_play("ui_click", {to_player=player_name, gain = 0.1}) local page = unified_inventory.current_page[player_name] local player_creative = unified_inventory.is_creative(player_name) @@ -201,25 +218,11 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) end end - if fields.searchbutton - or fields.key_enter_field == "searchbox" then - 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 - if ui.activefilter[player_name] ~= "" then - apply_new_filter(player, "", "nochange") - end - end - -- alternate buttons if not (fields.alternate or fields.alternate_prev) then return end - minetest.sound_play("click", + minetest.sound_play("ui_click", {to_player=player_name, gain = 0.1}) local item_name = unified_inventory.current_item[player_name] if not item_name then diff --git a/category.lua b/category.lua index d0fee5e..46b3e02 100644 --- a/category.lua +++ b/category.lua @@ -96,6 +96,9 @@ function unified_inventory.register_category(category_name, config) end update_category_list() end + +-- TODO: Mark these for removal. They are pretty much useless + function unified_inventory.set_category_symbol(category_name, symbol) ensure_category_exists(category_name) unified_inventory.registered_categories[category_name].symbol = symbol diff --git a/doc/mod_api.txt b/doc/mod_api.txt index ef87697..45eb432 100644 --- a/doc/mod_api.txt +++ b/doc/mod_api.txt @@ -1,7 +1,8 @@ unified_inventory API ===================== -This file provides information about the API of unified_inventory. +This file provides information about the API of unified_inventory +and can be viewed in Markdown readers. API revisions within unified_inventory can be checked using: @@ -23,7 +24,9 @@ Grouped by use-case, afterwards sorted alphabetically. Callbacks --------- -Register a callback that will be run whenever a craft is registered via unified_inventory.register_craft: +Register a callback that will be run whenever a craft is registered via unified_inventory.register_craft. +This callback is run before any recipe ingredients checks, hence it is also executed on recipes that are +purged after all mods finished loading. unified_inventory.register_on_craft_registered( function (item_name, options) @@ -163,68 +166,57 @@ Register a non-standard craft recipe: Categories ---------- -Register a new category: - The config table (second argument) is optional, and all its members are optional - See the unified_inventory.set_category_* functions for more details on the members of the config table + * `unified_inventory.register_category(name, def)` + * Registers a new category + * `name` (string): internal category name + * `def` (optional, table): also its fields are optional unified_inventory.register_category("category_name", { - symbol = "mod_name:item_name" or "texture.png", + symbol = source, + -- ^ Can be in the format "mod_name:item_name" or "texture.png", label = "Human Readable Label", index = 5, + -- ^ Categories are sorted by index. Lower numbers appear before higher ones. + -- By default, the name is translated to a number: AA -> 0.0101, ZZ -> 0.2626 + --- Predefined category indices: "all" = -2, "uncategorized" = -1 items = { "mod_name:item_name", "another_mod:different_item" } + -- ^ List of items within this category }) + * `unified_inventory.remove_category(name)` + * Removes an entire category -Add / override the symbol for a category: - The category does not need to exist first - The symbol can be an item name or a texture image - If unset this will default to "default:stick" +Modifier functions (to be removed) - unified_inventory.set_category_symbol("category_name", "mod_name:item_name" or "texture.png") + * `unified_inventory.set_category_symbol(name, source)` + * Changes the symbol of the category. The category does not need to exist yet. + * `name` (string): internal category name + * `source` (string, optional): `"mod_name:item_name"` or `"texture.png"`. + Defaults to `"default:stick"` if not specified. + * `unified_inventory.set_category_label(name, label)` + * Changes the human readable label of the category. + * `name` (string): internal category name + * `label` (string): human readable label. Defaults to the category name. + * `unified_inventory.set_category_index(name, index)` + * Changes the sorting index of the category. + * `name` (string): internal category name + * `index` (numeric): any real number -Add / override the human readable label for a category: - If unset this will default to the category name +Item management - unified_inventory.set_category_label("category_name", "Human Readable Label") + * ` unified_inventory.add_category_item(name, itemname)` + * Adds a single item to the category + * `itemname` (string): self-explanatory + * `unified_inventory.add_category_items(name, { itemname1, itemname2, ... }` + * Same as above but with multiple items + * `unified_inventory.remove_category_item(name, itemname)` + * Removes an item from the category + * `unified_inventory.find_category(itemname)` + * Looks up the first category containing this item + * Returns: category name (string) or nil + * `unified_inventory.find_categories(itemname)` + * Looks up the item name within all registered categories + * Returns: array of category names (table) -Add / override the sorting index of the category: - Must be a number, can also be negative (-5) or fractional (2.345) - This determines the position the category appears in the list of categories - The "all" meta-category has index -2, the "misc"/"uncategorized" meta-category has index -1, use a negative number smaller than these to make a category appear before these in the list - By default categories are sorted alphabetically with an index between 0.0101(AA) and 0.2626(ZZ) - - unified_inventory.set_category_index("category_name", 5) - -Add a single item to a category: - - unified_inventory.add_category_item("category_name", "mod_name:item_name") - -Add multiple items to a category: - - unified_inventory.add_category_items("category_name", { - "mod_name:item_name", - "another_mod:different_item" - }) - -Remove an item from a category: - - unified_inventory.remove_category_item("category_name", "mod_name:item_name") - -Remove a category entirely: - - unified_inventory.remove_category("category_name") - -Finding existing items in categories: - This will find the first category an item exists in - It should be used for checking if an item is catgorised - Returns "category_name" or nil - - unified_inventory.find_category("mod_name:item_name") - - - This will find all the categories an item exists in - Returns a number indexed table (list) of category names - - unified_inventory.find_categories("mod_name:item_name") diff --git a/group.lua b/group.lua index 2bc8e2f..1483b3c 100644 --- a/group.lua +++ b/group.lua @@ -1,4 +1,5 @@ local S = minetest.get_translator("unified_inventory") +local ui = unified_inventory function unified_inventory.extract_groupnames(groupname) local specname = ItemStack(groupname):get_name() @@ -26,6 +27,7 @@ end -- It may be a comma-separated list of group names. This is really a -- "group:..." ingredient specification, minus the "group:" prefix. +-- TODO Replace this with the more efficient spec matcher (below) local function compute_group_item(group_name_list) local group_names = group_name_list:split(",") local candidate_items = {} @@ -84,3 +86,61 @@ function unified_inventory.get_group_item(group_name) return group_item_cache[group_name] end + +--[[ +This is for filtering known items by groups +e.g. find all items that match "group:flower,yellow" (flower AND yellow groups) +]] +local spec_matcher = {} +function unified_inventory.init_matching_cache() + 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 +end + +--[[ +Retrieves all matching items + +Arguments: + specname (string): Item name or group(s) to filter + +Output: + { + matchingitem1 = true, + ... + } +]] +function unified_inventory.get_matching_items(specname) + if specname:sub(1,6) ~= "group:" then + return { [specname] = true } + 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 diff --git a/init.lua b/init.lua index 8bb2360..cf2f5b6 100644 --- a/init.lua +++ b/init.lua @@ -52,6 +52,8 @@ unified_inventory = { list_img_offset = 0.13, standard_background = "bgcolor[#0000]background9[0,0;1,1;ui_formbg_9_sliced.png;true;16]", + hide_disabled_buttons = minetest.settings:get_bool("unified_inventory_hide_disabled_buttons", false), + version = 4 } diff --git a/internal.lua b/internal.lua index 938ca19..0903d86 100644 --- a/internal.lua +++ b/internal.lua @@ -52,9 +52,11 @@ local function formspec_tab_buttons(player, formspec, style) local filtered_inv_buttons = {} - for i, def in pairs(ui.buttons) do + for _, def in pairs(ui.buttons) do if not (style.is_lite_mode and def.hide_lite) then - table.insert(filtered_inv_buttons, def) + if def.condition == nil or def.condition(player) or not ui.hide_disabled_buttons then + table.insert(filtered_inv_buttons, def) + end end end @@ -71,13 +73,14 @@ local function formspec_tab_buttons(player, formspec, style) 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 + if (def.condition == nil or def.condition(player)) 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, @@ -88,9 +91,10 @@ local function formspec_tab_buttons(player, formspec, style) end formspec[n] = "scroll_container_end[]" if needs_scrollbar then + local total_rows = math.ceil(#filtered_inv_buttons / style.main_button_cols) 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 + (total_rows - style.main_button_rows) * style.btn_spc * 10 ) 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 @@ -270,9 +274,11 @@ local function formspec_add_item_browser(player, formspec, ui_peruser) 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.1 + ui_peruser.btn_spc * 2, + formspec[n] = "style[page_number;content_offset=0]" + formspec[n + 1] = string.format("image_button[%f,%f;%f,0.4;;page_number;%s: %s;false;false;]", + ui_peruser.page_buttons_x, + ui_peruser.page_buttons_y + ui_peruser.btn_spc * 2 - 0.1, + ui_peruser.btn_spc * (bn - 1) + ui_peruser.btn_size, F(S("Page")), S("@1 of @2",page2,pagemax)) end @@ -329,7 +335,7 @@ function ui.set_inventory_formspec(player, page) end end -local function valid_def(def) +function ui.is_itemdef_listable(def) return (not def.groups.not_in_creative_inventory or def.groups.not_in_creative_inventory == 0) and def.description @@ -342,9 +348,11 @@ function ui.apply_filter(player, filter, search_dir) return false end local player_name = player:get_player_name() + local lfilter = string.lower(filter) local ffilter if lfilter:sub(1, 6) == "group:" then + -- Group filter: all groups of the item must match local groups = lfilter:sub(7):split(",") ffilter = function(name, def) for _, group in ipairs(groups) do @@ -356,6 +364,7 @@ function ui.apply_filter(player, filter, search_dir) return true end else + -- Name filter: fuzzy match item names and descriptions local player_info = minetest.get_player_information(player_name) local lang = player_info and player_info.lang_code or "" @@ -368,35 +377,41 @@ function ui.apply_filter(player, filter, search_dir) or llocaldesc and string.find(llocaldesc, lfilter, 1, true) end end - ui.filtered_items_list[player_name]={} + + local is_itemdef_listable = ui.is_itemdef_listable + local filtered_items = {} + local category = ui.current_category[player_name] or 'all' if category == 'all' then for name, def in pairs(minetest.registered_items) do - if valid_def(def) + if is_itemdef_listable(def) and ffilter(name, def) then - table.insert(ui.filtered_items_list[player_name], name) + table.insert(filtered_items, name) end end elseif category == 'uncategorized' then for name, def in pairs(minetest.registered_items) do - if (not ui.find_category(name)) - and valid_def(def) + if is_itemdef_listable(def) + and not ui.find_category(name) and ffilter(name, def) then - table.insert(ui.filtered_items_list[player_name], name) + table.insert(filtered_items, name) end end else - for name,exists in pairs(ui.registered_category_items[category]) do + -- Any other category is selected + for name, exists in pairs(ui.registered_category_items[category]) do local def = minetest.registered_items[name] if exists and def - and valid_def(def) + and is_itemdef_listable(def) and ffilter(name, def) then - table.insert(ui.filtered_items_list[player_name], name) + table.insert(filtered_items, name) end end end - table.sort(ui.filtered_items_list[player_name]) - ui.filtered_items_list_size[player_name] = #ui.filtered_items_list[player_name] + table.sort(filtered_items) + + ui.filtered_items_list_size[player_name] = #filtered_items + ui.filtered_items_list[player_name] = filtered_items ui.current_index[player_name] = 1 ui.activefilter[player_name] = filter ui.active_search_direction[player_name] = search_dir diff --git a/locale/unified_inventory.de.tr b/locale/unified_inventory.de.tr index 474049c..f1b1670 100644 --- a/locale/unified_inventory.de.tr +++ b/locale/unified_inventory.de.tr @@ -7,17 +7,17 @@ Bag @1=Tasche @1 Small Bag=Kleine Tasche Medium Bag=Mittelgroße Tasche Large Bag=Große Tasche -All Items= -Misc. Items= -Plant Life= -Building Materials= -Tools= -Minerals and Metals= -Environment and Worldgen= -Lighting= +All Items=Alle Gegenstände +Misc. Items=Sonstige Gegenstände +Plant Life=Pfanzenwelt +Building Materials=Baumaterialien +Tools=Werkzeuge +Minerals and Metals=Minerale und Metalle +Environment and Worldgen=Umwelt und Welterstellung +Lighting=Beleuchtung and = und -Scroll categories left= -Scroll categories right= +Scroll categories left=Kategorien nach links blättern +Scroll categories right=Kategorien nach rechts blättern Search=Suchen Reset search and display everything=Suche zurücksetzen und alles anzeigen First page=Erste Seite @@ -76,10 +76,10 @@ Waypoints=Wegpunkte Select Waypoint #@1=Wegpunkt Nr. @1 auswählen Waypoint @1=Wegpunkt Nr. @1 Set waypoint to current location=Setze Wegpunkt zur derzeitigen Position -Hide waypoint= -Show waypoint= -Hide coordinates= -Show coordinates= +Hide waypoint=Wegpunkt verstecken +Show waypoint=Wegpunkt zeigen +Hide coordinates=Koordinaten verstecken +Show coordinates=Koordinaten zeigen Change color of waypoint display=Farbe der Darstellung der Wegpunkte ändern Edit waypoint name=Name des Wegpunkts ändern Waypoint active=Wegpunkt aktiv diff --git a/match_craft.lua b/match_craft.lua index 2dd40b0..b2d18ec 100644 --- a/match_craft.lua +++ b/match_craft.lua @@ -126,25 +126,18 @@ Example output: } --]] function unified_inventory.find_usable_items(inv_items, craft_items) - local get_group = minetest.get_item_group local result = {} for craft_item in pairs(craft_items) do - local group = craft_item:match("^group:(.+)") - local found = {} + -- may specify group:type1,type2 + local items = unified_inventory.get_matching_items(craft_item) - if group ~= nil then - for inv_item in pairs(inv_items) do - if get_group(inv_item, group) > 0 then - found[inv_item] = true - end - end - else - if inv_items[craft_item] ~= nil then - found[craft_item] = true + local found = {} + for itemname, _ in pairs(items) do + if inv_items[itemname] then + found[itemname] = true end end - result[craft_item] = found end diff --git a/mod.conf b/mod.conf index 3d27d29..0f03231 100644 --- a/mod.conf +++ b/mod.conf @@ -1,6 +1,6 @@ name = unified_inventory -optional_depends = default, creative, sfinv, datastorage, farming +optional_depends = default, creative, sfinv, datastorage description = """ Unified Inventory replaces the default survival and creative inventory. It adds a nicer interface and a number of features, such as a crafting guide. diff --git a/register.lua b/register.lua index fa60422..3915f2c 100644 --- a/register.lua +++ b/register.lua @@ -49,7 +49,7 @@ ui.register_button("misc_set_day", { action = function(player) local player_name = player:get_player_name() if minetest.check_player_privs(player_name, {settime=true}) then - minetest.sound_play("birds", + minetest.sound_play("ui_morning", {to_player=player_name, gain = 1.0}) minetest.set_timeofday((6000 % 24000) / 24000) minetest.chat_send_player(player_name, @@ -73,7 +73,7 @@ ui.register_button("misc_set_night", { action = function(player) local player_name = player:get_player_name() if minetest.check_player_privs(player_name, {settime=true}) then - minetest.sound_play("owl", + minetest.sound_play("ui_owl", {to_player=player_name, gain = 1.0}) minetest.set_timeofday((21000 % 24000) / 24000) minetest.chat_send_player(player_name, @@ -134,14 +134,14 @@ ui.register_page("craft", { local n=#formspec+1 if ui.trash_enabled or ui.is_creative(player_name) or minetest.get_player_privs(player_name).give then - formspec[n] = string.format("label[%f,%f;%s]", craftx + 6.45, crafty + 2.4, F(S("Trash:"))) + formspec[n] = string.format("label[%f,%f;%s]", craftx + 6.35, crafty + 2.3, F(S("Trash:"))) formspec[n+1] = ui.make_trash_slot(craftx + 6.25, crafty + 2.5) n=n + 2 end if ui.is_creative(player_name) then formspec[n] = ui.single_slot(craftx - 2.5, crafty + 2.5) - formspec[n+1] = string.format("label[%f,%f;%s]", craftx - 2.3, crafty + 2.4,F(S("Refill:"))) + formspec[n+1] = string.format("label[%f,%f;%s]", craftx - 2.4, crafty + 2.3, F(S("Refill:"))) formspec[n+2] = string.format("list[detached:%srefill;main;%f,%f;1,1;]", F(player_name), craftx - 2.5 + ui.list_img_offset, crafty + 2.5 + ui.list_img_offset) end @@ -158,11 +158,9 @@ ui.register_page("craft", { local function stack_image_button(x, y, w, h, buttonname_prefix, item) local name = item:get_name() - local count = item:get_count() - local wear = item:get_wear() local description = item:get_meta():get_string("description") local show_is_group = false - local displayitem = name.." "..count.." "..wear + local displayitem = item:to_string() local selectitem = name if name:sub(1, 6) == "group:" then local group_name = name:sub(7) @@ -196,6 +194,9 @@ local function stack_image_button(x, y, w, h, buttonname_prefix, item) return button end +-- The recipe text contains parameters, hence they can yet not be translated. +-- Instead, use a dummy translation call so that it can be picked up by the +-- static parsing of the translation string update script local recipe_text = { recipe = NS("Recipe @1 of @2"), usage = NS("Usage @1 of @2"), diff --git a/settingtypes.txt b/settingtypes.txt index 27768ac..5ab7f84 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -10,5 +10,8 @@ unified_inventory_bags (Enable bags) bool true #and the give privilege. unified_inventory_trash (Enable trash) bool true +#If enabled, disabled buttons will be hidden instead of grayed out. +unified_inventory_hide_disabled_buttons (Hide disabled buttons) bool false -unified_inventory_automatic_categorization (Items automatically added to categories) bool true \ No newline at end of file + +unified_inventory_automatic_categorization (Items automatically added to categories) bool true diff --git a/sounds/birds.ogg b/sounds/birds.ogg deleted file mode 100644 index 4a93395..0000000 Binary files a/sounds/birds.ogg and /dev/null differ diff --git a/sounds/owl.ogg b/sounds/owl.ogg deleted file mode 100644 index f30d0b3..0000000 Binary files a/sounds/owl.ogg and /dev/null differ diff --git a/sounds/ui_click.ogg b/sounds/ui_click.ogg new file mode 100644 index 0000000..0c9fe31 Binary files /dev/null and b/sounds/ui_click.ogg differ diff --git a/sounds/ui_morning.ogg b/sounds/ui_morning.ogg new file mode 100644 index 0000000..7fb8c22 Binary files /dev/null and b/sounds/ui_morning.ogg differ diff --git a/sounds/ui_owl.ogg b/sounds/ui_owl.ogg new file mode 100644 index 0000000..a1433f9 Binary files /dev/null and b/sounds/ui_owl.ogg differ diff --git a/textures/ui_teleport.png b/textures/ui_teleport.png new file mode 100644 index 0000000..8ea5d20 Binary files /dev/null and b/textures/ui_teleport.png differ diff --git a/waypoints.lua b/waypoints.lua index 1496cdd..8dbb175 100644 --- a/waypoints.lua +++ b/waypoints.lua @@ -140,36 +140,49 @@ ui.register_page("waypoints", { -- Main buttons: local btnlist = { - set_waypoint = { - "ui_waypoint_set_icon.png", - S("Set waypoint to current location") - }, - toggle_waypoint = { + -- 1. formspec name + -- 2. button image + -- 3. translation text + { + "toggle_waypoint", waypoint.active and "ui_on_icon.png" or "ui_off_icon.png", waypoint.active and S("Hide waypoint") or S("Show waypoint") }, - toggle_display_pos = { + { + "rename_waypoint", + "ui_pencil_icon.png", + S("Edit waypoint name") + }, + { + "set_waypoint", + "ui_waypoint_set_icon.png", + S("Set waypoint to current location") + }, + { + "toggle_display_pos", waypoint.display_pos and "ui_green_icon_background.png^ui_xyz_icon.png" or "ui_red_icon_background.png^ui_xyz_icon.png^(ui_no.png^[transformR90)", waypoint.display_pos and S("Hide coordinates") or S("Show coordinates") }, - toggle_color = { + { + "toggle_color", "ui_circular_arrows_icon.png", S("Change color of waypoint display") }, - rename_waypoint = { - "ui_pencil_icon.png", - S("Edit waypoint name") - } } + if minetest.get_player_privs(player_name).teleport then + table.insert(btnlist, { + "teleport_waypoint", + "ui_teleport.png", + S("Teleport to waypoint") + }) + end - local x = 4 - for name, def in pairs(btnlist) do + for i, def in pairs(btnlist) do formspec[n] = string.format("image_button[%f,%f;%f,%f;%s;%s%i;]", - wp_buttons_rj - ui.style_full.btn_spc * x, wp_bottom_row, + wp_buttons_rj + ui.style_full.btn_spc * (i - #btnlist), wp_bottom_row, ui.style_full.btn_size, ui.style_full.btn_size, - def[1], name, sel) - formspec[n+1] = "tooltip["..name..sel..";"..F(def[2]).."]" - x = x - 1 + def[2], def[1], sel) + formspec[n+1] = "tooltip["..def[1]..sel..";"..F(def[3]).."]" n = n + 2 end @@ -313,6 +326,13 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) update_formspec = true end + if fields["teleport_waypoint" .. i] and waypoint.world_pos then + if minetest.get_player_privs(player_name).teleport then + minetest.sound_play("teleport", {to_player = player_name}) + player:set_pos(waypoint.world_pos) + end + end + if hit then -- Save first waypoints.data[i] = waypoint @@ -323,6 +343,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) update_hud(player, waypoints, temp, i) end if update_formspec then + minetest.sound_play("ui_click", {to_player=player_name, gain = 0.1}) ui.set_inventory_formspec(player, "waypoints") end