From 6e6383f0821908e762d8fde8a13d7a083a642553 Mon Sep 17 00:00:00 2001 From: Oversword Date: Mon, 5 Apr 2021 19:07:14 +0100 Subject: [PATCH] Add filter-by-category functionality (#171) - Added API for configuring categories - Added display for categories above page - Reduced height of page by 1 row to make room for categories - Added L/R scroll through when there are more categories than columns - Added pre-filter methods for categories and uncategorised items - Added categories for (most) items in the default game Co-authored-by: Oversword --- .luacheckrc | 1 + callbacks.lua | 38 ++ category.lua | 149 +++++++ default-categories.lua | 704 +++++++++++++++++++++++++++++++ doc/mod_api.txt | 69 +++ init.lua | 19 +- internal.lua | 104 ++++- settingtypes.txt | 3 + textures/ui_category_all.png | Bin 0 -> 1233 bytes textures/ui_category_none.png | Bin 0 -> 7966 bytes textures/ui_smallbg_9_sliced.png | Bin 0 -> 139 bytes 11 files changed, 1069 insertions(+), 18 deletions(-) create mode 100644 category.lua create mode 100644 default-categories.lua create mode 100644 textures/ui_category_all.png create mode 100644 textures/ui_category_none.png create mode 100644 textures/ui_smallbg_9_sliced.png diff --git a/.luacheckrc b/.luacheckrc index 9fb6a7c..e6fec97 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -14,6 +14,7 @@ read_globals = { "ItemStack", "datastorage", "hb", + "doors", } files["callbacks.lua"].ignore = { "player", "draw_lite_mode" } diff --git a/callbacks.lua b/callbacks.lua index bc90237..1f43e39 100644 --- a/callbacks.lua +++ b/callbacks.lua @@ -19,6 +19,8 @@ minetest.register_on_joinplayer(function(player) unified_inventory.active_search_direction[player_name] = "nochange" unified_inventory.apply_filter(player, "", "nochange") unified_inventory.current_searchbox[player_name] = "" + unified_inventory.current_category[player_name] = "all" + unified_inventory.current_category_scroll[player_name] = 0 unified_inventory.alternate[player_name] = 1 unified_inventory.current_item[player_name] = nil unified_inventory.current_craft_direction[player_name] = "recipe" @@ -69,6 +71,41 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) unified_inventory.current_searchbox[player_name] = fields.searchbox end + + local clicked_category + for name, value in pairs(fields) do + local category_name = string.match(name, "^category_(.+)$") + if category_name then + clicked_category = category_name + break + end + end + + if clicked_category + and clicked_category ~= unified_inventory.current_category[player_name] then + unified_inventory.current_category[player_name] = clicked_category + unified_inventory.apply_filter(player, unified_inventory.current_searchbox[player_name], "nochange") + unified_inventory.set_inventory_formspec(player, + 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, + unified_inventory.current_page[player_name]) + end + end + for i, def in pairs(unified_inventory.buttons) do if fields[def.name] then def.action(player) @@ -126,6 +163,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) clicked_item = unified_inventory.demangle_for_formspec(mangled_item) if string.sub(clicked_item, 1, 6) == "group:" then -- Change search filter to this group + unified_inventory.current_category[player_name] = "all" apply_new_filter(player, clicked_item, new_dir) return end diff --git a/category.lua b/category.lua new file mode 100644 index 0000000..72e4038 --- /dev/null +++ b/category.lua @@ -0,0 +1,149 @@ +local S = minetest.get_translator("unified_inventory") + +unified_inventory.registered_categories = {} +unified_inventory.registered_category_items = {} +unified_inventory.category_list = {} + +local function char_to_sort_index(char_code) + if char_code <= 32 then + -- Command codes, no thanks + return 0 + end + if char_code <= 64 then + -- Sorts numbers, and some punctuation, after letters + return char_code + end + if char_code >= 158 then + -- Out of sortable range + return 0 + end + if char_code > 122 then + -- Avoids overlap with {, |, } and ~ + return char_code - 58 + end + if char_code > 96 then + -- Normalises lowercase with uppercase + return char_code - 96 + end + return char_code - 64 +end + +local function string_to_sort_index(str) + local max_chars = 5 + local power = 100 + local index = 0 + for i=1,math.min(#str, max_chars) do + index = index + (char_to_sort_index(string.byte(str, i))/(power^i)) + end + return index +end + +function update_category_list() + local category_list = {} + table.insert(category_list, { + name = "all", + label = S("All Items"), + symbol = "ui_category_all.png", + index = -2, + }) + table.insert(category_list, { + name = "uncategorized", + label = S("Misc. Items"), + symbol = "ui_category_none.png", + index = -1, + }) + for category, def in pairs(unified_inventory.registered_categories) do + table.insert(category_list, { + name = category, + label = def.label or category, + symbol = def.symbol, + index = def.index or -- sortby defined order + string_to_sort_index(category) -- or do a rudimentary alphabetical sort + }) + end + table.sort(category_list, function (a,b) + return a.index < b.index + end) + unified_inventory.category_list = category_list +end + +local function ensure_category_exists(category_name) + if not unified_inventory.registered_categories[category_name] then + unified_inventory.registered_categories[category_name] = { + symbol = "default:stick", + label = category_name + } + end + if not unified_inventory.registered_category_items[category_name] then + unified_inventory.registered_category_items[category_name] = {} + end +end + +function unified_inventory.register_category(category_name, config) + ensure_category_exists(category_name) + if config and config.symbol then + unified_inventory.set_category_symbol(category_name, config.symbol) + end + if config and config.label then + unified_inventory.set_category_label(category_name, config.label) + end + if config and config.index then + unified_inventory.set_category_index(category_name, config.index) + end + if config and config.items then + unified_inventory.add_category_items(category_name, config.items) + end + update_category_list() +end +function unified_inventory.set_category_symbol(category_name, symbol) + ensure_category_exists(category_name) + unified_inventory.registered_categories[category_name].symbol = symbol + update_category_list() +end +function unified_inventory.set_category_label(category_name, label) + ensure_category_exists(category_name) + unified_inventory.registered_categories[category_name].label = label + update_category_list() +end +function unified_inventory.set_category_index(category_name, index) + ensure_category_exists(category_name) + unified_inventory.registered_categories[category_name].index = index + update_category_list() +end +function unified_inventory.add_category_item(category_name, item) + ensure_category_exists(category_name) + unified_inventory.registered_category_items[category_name][item] = true +end +function unified_inventory.add_category_items(category_name, items) + for _,item in ipairs(items) do + unified_inventory.add_category_item(category_name, item) + end +end + +function unified_inventory.remove_category_item(category_name, item) + unified_inventory.registered_category_items[category_name][item] = nil +end +function unified_inventory.remove_category(category_name) + unified_inventory.registered_categories[category_name] = nil + unified_inventory.registered_category_items[category_name] = nil + update_category_list() +end + +function unified_inventory.find_category(item) + -- Returns the first category the item exists in + -- Best for checking if an item has any category at all + for category, items in pairs(unified_inventory.registered_category_items) do + if items[item] then return category end + end +end +function unified_inventory.find_categories(item) + -- Returns all the categories the item exists in + -- Best for listing all categories + local categories = {} + for category, items in pairs(unified_inventory.registered_category_items) do + if items[item] then + table.insert(categories, category) + end + end + return categories +end diff --git a/default-categories.lua b/default-categories.lua new file mode 100644 index 0000000..57d3e88 --- /dev/null +++ b/default-categories.lua @@ -0,0 +1,704 @@ +local S = minetest.get_translator("unified_inventory") + +unified_inventory.register_category('plants', { + symbol = "flowers:tulip", + label = S("Plant Life") +}) +unified_inventory.register_category('building', { + symbol = "default:brick", + label = S("Building Materials") +}) +unified_inventory.register_category('tools', { + symbol = "default:pick_diamond", + label = S("Tools") +}) +unified_inventory.register_category('minerals', { + symbol = "default:iron_lump", + label = S("Minerals and Metals") +}) +unified_inventory.register_category('environment', { + symbol = "default:dirt_with_grass", + label = S("Environment and Worldgen") +}) +unified_inventory.register_category('lighting', { + symbol = "default:torch", + label = S("Lighting") +}) + + +if unified_inventory.automatic_categorization then + minetest.register_on_mods_loaded(function() + + -- Add biome nodes to environment category + for _,def in pairs(minetest.registered_biomes) do + local env_nodes = { + def.node_riverbed, def.node_top, def.node_filler, def.node_dust, + } + for i,node in pairs(env_nodes) do + if node then + unified_inventory.add_category_item('environment', node) + end + end + end + + -- Add minable ores to minerals and everything else (pockets of stone & sand variations) to environment + for _,item in pairs(minetest.registered_ores) do + if item.ore_type == "scatter" then + local drop = minetest.registered_nodes[item.ore].drop + if drop and drop ~= "" then + unified_inventory.add_category_item('minerals', item.ore) + unified_inventory.add_category_item('minerals', drop) + else + unified_inventory.add_category_item('environment', item.ore) + end + else + unified_inventory.add_category_item('environment', item.ore) + end + end + + -- Add items by item definition + for name, def in pairs(minetest.registered_items) do + local group = def.groups or {} + if not group.not_in_creative_inventory then + if group.stair or + group.slab or + group.wall or + group.fence then + unified_inventory.add_category_item('building', name) + elseif group.flora or + group.flower or + group.seed or + group.leaves or + group.sapling or + group.tree then + unified_inventory.add_category_item('plants', name) + elseif def.type == 'tool' then + unified_inventory.add_category_item('tools', name) + elseif def.liquidtype == 'source' then + unified_inventory.add_category_item('environment', name) + elseif def.light_source and def.light_source > 0 then + unified_inventory.add_category_item('lighting', name) + elseif group.door or + minetest.global_exists("doors") and ( + doors.registered_doors and doors.registered_doors[name..'_a'] or + doors.registered_trapdoors and doors.registered_trapdoors[name] + ) then + unified_inventory.add_category_item('building', name) + end + end + end + end) +end + +-- [[ +unified_inventory.add_category_items('plants', { + "default:dry_grass_5", + "default:acacia_sapling", + "default:blueberry_bush_sapling", + "default:grass_2", + "default:pine_bush_stem", + "default:leaves", + "default:pine_needles", + "default:cactus", + "default:junglegrass", + "default:pine_sapling", + "default:sapling", + "default:bush_stem", + "default:dry_grass_2", + "default:fern_1", + "default:grass_3", + "default:marram_grass_1", + "default:pine_tree", + "default:dry_grass_3", + "default:dry_shrub", + "default:grass_4", + "default:marram_grass_2", + "default:jungleleaves", + "default:apple", + "default:tree", + "default:aspen_tree", + "default:bush_sapling", + "default:grass_5", + "default:blueberry_bush_leaves_with_berries", + "default:acacia_bush_sapling", + "default:grass_1", + "default:aspen_leaves", + "default:marram_grass_3", + "default:large_cactus_seedling", + "default:junglesapling", + "default:dry_grass_4", + "default:acacia_bush_stem", + "default:papyrus", + "default:pine_bush_needles", + "default:bush_leaves", + "default:fern_3", + "default:aspen_sapling", + "default:acacia_tree", + "default:apple_mark", + "default:acacia_leaves", + "default:jungletree", + "default:dry_grass_1", + "default:acacia_bush_leaves", + "default:emergent_jungle_sapling", + "default:fern_2", + "default:blueberries", + "default:sand_with_kelp", + "default:blueberry_bush_leaves", + "default:pine_bush_sapling", + + "farming:cotton", + "farming:cotton_1", + "farming:cotton_2", + "farming:cotton_3", + "farming:cotton_4", + "farming:cotton_5", + "farming:cotton_6", + "farming:cotton_7", + "farming:cotton_8", + "farming:cotton_wild", + "farming:seed_cotton", + "farming:seed_wheat", + "farming:straw", + "farming:wheat", + "farming:wheat_1", + "farming:wheat_2", + "farming:wheat_3", + "farming:wheat_4", + "farming:wheat_5", + "farming:wheat_6", + "farming:wheat_7", + "farming:wheat_8", + + "flowers:chrysanthemum_green", + "flowers:dandelion_white", + "flowers:dandelion_yellow", + "flowers:geranium", + "flowers:mushroom_brown", + "flowers:mushroom_red", + "flowers:rose", + "flowers:tulip", + "flowers:tulip_black", + "flowers:viola", + "flowers:waterlily", + "flowers:waterlily_waving", +}) + +unified_inventory.add_category_items('tools', { + "default:sword_diamond", + "default:axe_diamond", + "default:shovel_diamond", + "default:axe_steel", + "default:shovel_mese", + "default:sword_wood", + "default:pick_bronze", + "default:axe_stone", + "default:sword_stone", + "default:pick_stone", + "default:shovel_stone", + "default:sword_mese", + "default:shovel_bronze", + "default:sword_bronze", + "default:axe_bronze", + "default:shovel_steel", + "default:sword_steel", + "default:axe_mese", + "default:shovel_wood", + "default:pick_mese", + "default:axe_wood", + "default:pick_diamond", + "default:pick_wood", + "default:pick_steel", + + "farming:hoe_bronze", + "farming:hoe_diamond", + "farming:hoe_mese", + "farming:hoe_steel", + "farming:hoe_stone", + "farming:hoe_wood", + + "fire:flint_and_steel", + "map:mapping_kit", + "screwdriver:screwdriver", + + "fireflies:bug_net", + "bucket:bucket_empty", + + "binoculars:binoculars", + "default:skeleton_key", +}) + +unified_inventory.add_category_items('minerals', { + "default:stone_with_copper", + "default:stone_with_gold", + "default:stone_with_iron", + "default:copper_ingot", + "default:copper_lump", + "default:gold_lump", + "default:diamondblock", + "default:stone_with_diamond", + "default:stone_with_mese", + "default:steel_ingot", + "default:gold_ingot", + "default:iron_lump", + "default:tinblock", + "default:tin_lump", + "default:stone_with_tin", + "default:mese_crystal", + "default:diamond", + "default:bronze_ingot", + "default:mese", + "default:mese_crystal_fragment", + "default:copperblock", + "default:stone_with_coal", + "default:steelblock", + "default:tin_ingot", + "default:coalblock", + "default:coal_lump", + "default:bronzeblock", + "default:goldblock", + + "stairs:slab_bronzeblock", + "stairs:slab_copperblock", + "stairs:slab_steelblock", + "stairs:slab_tinblock", + "stairs:stair_bronzeblock", + "stairs:stair_copperblock", + "stairs:stair_inner_bronzeblock", + "stairs:stair_inner_copperblock", + "stairs:stair_inner_steelblock", + "stairs:stair_inner_tinblock", + "stairs:stair_outer_bronzeblock", + "stairs:stair_outer_copperblock", + "stairs:stair_outer_steelblock", + "stairs:stair_outer_tinblock", + "stairs:stair_steelblock", + "stairs:stair_tinblock", +}) + +unified_inventory.add_category_items('building', { + "default:fence_rail_aspen_wood", + "default:fence_rail_acacia_wood", + "default:fence_junglewood", + "default:fence_rail_junglewood", + "default:fence_aspen_wood", + "default:fence_pine_wood", + "default:fence_rail_wood", + "default:fence_rail_pine_wood", + "default:fence_acacia_wood", + "default:junglewood", + "default:acacia_wood", + "default:aspen_wood", + "default:fence_wood", + "default:pine_wood", + "default:silver_sandstone", + "default:desert_sandstone", + "default:sandstone_block", + "default:desert_sandstone_brick", + "default:stone_block", + "default:stonebrick", + "default:obsidian_glass", + "default:desert_sandstone_block", + "default:silver_sandstone_brick", + "default:brick", + "default:obsidianbrick", + "default:sandstonebrick", + "default:sandstone", + "default:desert_stone_block", + "default:silver_sandstone_block", + "default:wood", + "default:obsidian_block", + "default:glass", + "default:clay_brick", + "default:desert_stonebrick", + "default:desert_cobble", + "default:cobble", + "default:mossycobble", + + "doors:door_glass", + "doors:door_glass_a", + "doors:door_glass_b", + "doors:door_glass_c", + "doors:door_glass_d", + "doors:door_obsidian_glass", + "doors:door_obsidian_glass_a", + "doors:door_obsidian_glass_b", + "doors:door_obsidian_glass_c", + "doors:door_obsidian_glass_d", + "doors:door_steel", + "doors:door_steel_a", + "doors:door_steel_b", + "doors:door_steel_c", + "doors:door_steel_d", + "doors:door_wood", + "doors:door_wood_a", + "doors:door_wood_b", + "doors:door_wood_c", + "doors:door_wood_d", + "doors:gate_acacia_wood_closed", + "doors:gate_acacia_wood_open", + "doors:gate_aspen_wood_closed", + "doors:gate_aspen_wood_open", + "doors:gate_junglewood_closed", + "doors:gate_junglewood_open", + "doors:gate_pine_wood_closed", + "doors:gate_pine_wood_open", + "doors:gate_wood_closed", + "doors:gate_wood_open", + "doors:hidden", + "doors:trapdoor", + "doors:trapdoor_open", + "doors:trapdoor_steel", + "doors:trapdoor_steel_open", + + "stairs:slab_bronzeblock", + "stairs:slab_copperblock", + "stairs:slab_steelblock", + "stairs:slab_tinblock", + "stairs:stair_bronzeblock", + "stairs:stair_copperblock", + "stairs:stair_inner_bronzeblock", + "stairs:stair_inner_copperblock", + "stairs:stair_inner_steelblock", + "stairs:stair_inner_tinblock", + "stairs:stair_outer_bronzeblock", + "stairs:stair_outer_copperblock", + "stairs:stair_outer_steelblock", + "stairs:stair_outer_tinblock", + "stairs:stair_steelblock", + "stairs:stair_tinblock", + + "stairs:slab_acacia_wood", + "stairs:slab_aspen_wood", + "stairs:slab_brick", + "stairs:slab_cobble", + "stairs:slab_desert_cobble", + "stairs:slab_desert_sandstone", + "stairs:slab_desert_sandstone_block", + "stairs:slab_desert_sandstone_brick", + "stairs:slab_desert_stone", + "stairs:slab_desert_stone_block", + "stairs:slab_desert_stonebrick", + "stairs:slab_glass", + "stairs:slab_goldblock", + "stairs:slab_ice", + "stairs:slab_junglewood", + "stairs:slab_mossycobble", + "stairs:slab_obsidian", + "stairs:slab_obsidian_block", + "stairs:slab_obsidian_glass", + "stairs:slab_obsidianbrick", + "stairs:slab_pine_wood", + "stairs:slab_sandstone", + "stairs:slab_sandstone_block", + "stairs:slab_sandstonebrick", + "stairs:slab_silver_sandstone", + "stairs:slab_silver_sandstone_block", + "stairs:slab_silver_sandstone_brick", + "stairs:slab_snowblock", + "stairs:slab_stone", + "stairs:slab_stone_block", + "stairs:slab_stonebrick", + "stairs:slab_straw", + "stairs:slab_wood", + "stairs:stair_acacia_wood", + "stairs:stair_aspen_wood", + "stairs:stair_brick", + "stairs:stair_cobble", + "stairs:stair_desert_cobble", + "stairs:stair_desert_sandstone", + "stairs:stair_desert_sandstone_block", + "stairs:stair_desert_sandstone_brick", + "stairs:stair_desert_stone", + "stairs:stair_desert_stone_block", + "stairs:stair_desert_stonebrick", + "stairs:stair_glass", + "stairs:stair_goldblock", + "stairs:stair_ice", + "stairs:stair_inner_acacia_wood", + "stairs:stair_inner_aspen_wood", + "stairs:stair_inner_brick", + "stairs:stair_inner_cobble", + "stairs:stair_inner_desert_cobble", + "stairs:stair_inner_desert_sandstone", + "stairs:stair_inner_desert_sandstone_block", + "stairs:stair_inner_desert_sandstone_brick", + "stairs:stair_inner_desert_stone", + "stairs:stair_inner_desert_stone_block", + "stairs:stair_inner_desert_stonebrick", + "stairs:stair_inner_glass", + "stairs:stair_inner_goldblock", + "stairs:stair_inner_ice", + "stairs:stair_inner_junglewood", + "stairs:stair_inner_mossycobble", + "stairs:stair_inner_obsidian", + "stairs:stair_inner_obsidian_block", + "stairs:stair_inner_obsidian_glass", + "stairs:stair_inner_obsidianbrick", + "stairs:stair_inner_pine_wood", + "stairs:stair_inner_sandstone", + "stairs:stair_inner_sandstone_block", + "stairs:stair_inner_sandstonebrick", + "stairs:stair_inner_silver_sandstone", + "stairs:stair_inner_silver_sandstone_block", + "stairs:stair_inner_silver_sandstone_brick", + "stairs:stair_inner_snowblock", + "stairs:stair_inner_stone", + "stairs:stair_inner_stone_block", + "stairs:stair_inner_stonebrick", + "stairs:stair_inner_straw", + "stairs:stair_inner_wood", + "stairs:stair_junglewood", + "stairs:stair_mossycobble", + "stairs:stair_obsidian", + "stairs:stair_obsidian_block", + "stairs:stair_obsidian_glass", + "stairs:stair_obsidianbrick", + "stairs:stair_outer_acacia_wood", + "stairs:stair_outer_aspen_wood", + "stairs:stair_outer_brick", + "stairs:stair_outer_cobble", + "stairs:stair_outer_desert_cobble", + "stairs:stair_outer_desert_sandstone", + "stairs:stair_outer_desert_sandstone_block", + "stairs:stair_outer_desert_sandstone_brick", + "stairs:stair_outer_desert_stone", + "stairs:stair_outer_desert_stone_block", + "stairs:stair_outer_desert_stonebrick", + "stairs:stair_outer_glass", + "stairs:stair_outer_goldblock", + "stairs:stair_outer_ice", + "stairs:stair_outer_junglewood", + "stairs:stair_outer_mossycobble", + "stairs:stair_outer_obsidian", + "stairs:stair_outer_obsidian_block", + "stairs:stair_outer_obsidian_glass", + "stairs:stair_outer_obsidianbrick", + "stairs:stair_outer_pine_wood", + "stairs:stair_outer_sandstone", + "stairs:stair_outer_sandstone_block", + "stairs:stair_outer_sandstonebrick", + "stairs:stair_outer_silver_sandstone", + "stairs:stair_outer_silver_sandstone_block", + "stairs:stair_outer_silver_sandstone_brick", + "stairs:stair_outer_snowblock", + "stairs:stair_outer_stone", + "stairs:stair_outer_stone_block", + "stairs:stair_outer_stonebrick", + "stairs:stair_outer_straw", + "stairs:stair_outer_wood", + "stairs:stair_pine_wood", + "stairs:stair_sandstone", + "stairs:stair_sandstone_block", + "stairs:stair_sandstonebrick", + "stairs:stair_silver_sandstone", + "stairs:stair_silver_sandstone_block", + "stairs:stair_silver_sandstone_brick", + "stairs:stair_snowblock", + "stairs:stair_stone", + "stairs:stair_stone_block", + "stairs:stair_stonebrick", + "stairs:stair_straw", + "stairs:stair_wood", + + "xpanes:bar", + "xpanes:bar_flat", + "xpanes:door_steel_bar", + "xpanes:door_steel_bar_a", + "xpanes:door_steel_bar_b", + "xpanes:door_steel_bar_c", + "xpanes:door_steel_bar_d", + "xpanes:obsidian_pane", + "xpanes:obsidian_pane_flat", + "xpanes:pane", + "xpanes:pane_flat", + "xpanes:trapdoor_steel_bar", + "xpanes:trapdoor_steel_bar_open", + + "walls:cobble", + "walls:desertcobble", + "walls:mossycobble", +}) + +unified_inventory.add_category_items('environment', { + "air", + "default:cave_ice", + "default:dirt_with_rainforest_litter", + "default:gravel", + "default:dry_dirt_with_dry_grass", + "default:permafrost", + "default:desert_stone", + "default:ice", + "default:dry_dirt", + "default:obsidian", + "default:sand", + "default:river_water_source", + "default:dirt_with_snow", + "default:dirt_with_grass", + "default:water_flowing", + "default:dirt", + "default:desert_sand", + "default:permafrost_with_moss", + "default:dirt_with_coniferous_litter", + "default:water_source", + "default:dirt_with_dry_grass", + "default:river_water_flowing", + "default:stone", + "default:snow", + "default:lava_flowing", + "default:lava_source", + "default:permafrost_with_stones", + "default:dirt_with_grass_footsteps", + "default:silver_sand", + "default:snowblock", + "default:clay", + + "farming:desert_sand_soil", + "farming:desert_sand_soil_wet", + "farming:dry_soil", + "farming:dry_soil_wet", + "farming:soil", + "farming:soil_wet", +}) + +unified_inventory.add_category_items('lighting', { + "default:mese_post_light_junglewood", + "default:torch_ceiling", + "default:meselamp", + "default:torch", + "default:mese_post_light_acacia_wood", + "default:mese_post_light", + "default:torch_wall", + "default:mese_post_light_pine_wood", + "default:mese_post_light_aspen_wood" +}) +--]] + + +--[[ UNCATEGORISED + + "farming:string", + + "beds:bed_bottom", + "beds:bed_top", + "beds:fancy_bed_bottom", + "beds:fancy_bed_top", + "boats:boat", + "bones:bones", + + "bucket:bucket_lava", + "bucket:bucket_river_water", + "bucket:bucket_water", + + "butterflies:butterfly_red", + "butterflies:butterfly_violet", + "butterflies:butterfly_white", + "butterflies:hidden_butterfly_red", + "butterflies:hidden_butterfly_violet", + "butterflies:hidden_butterfly_white", + + "carts:brakerail", + "carts:cart", + "carts:powerrail", + "carts:rail", + + "default:book", + "default:book_written", + "default:bookshelf", + "default:chest", + "default:chest_locked", + "default:chest_locked_open", + "default:chest_open", + "default:clay_lump", + "default:cloud", + "default:coral_brown", + "default:coral_cyan", + "default:coral_green", + "default:coral_orange", + "default:coral_pink", + "default:coral_skeleton", + "default:flint", + "default:furnace", + "default:furnace_active", + "default:key", + "default:ladder_steel", + "default:ladder_wood", + "default:obsidian_shard", + "default:paper", + "default:sign_wall_steel", + "default:sign_wall_wood", + "default:stick", + + "fire:basic_flame", + "fire:permanent_flame", + "fireflies:firefly", + "fireflies:firefly_bottle", + "fireflies:hidden_firefly", + + "ignore", + "unknown", + + "tnt:boom", + "tnt:gunpowder", + "tnt:gunpowder_burning", + "tnt:tnt", + "tnt:tnt_burning", + "tnt:tnt_stick", + + "vessels:drinking_glass", + "vessels:glass_bottle", + "vessels:glass_fragments", + "vessels:shelf", + "vessels:steel_bottle", + + "dye:black", + "dye:blue", + "dye:brown", + "dye:cyan", + "dye:dark_green", + "dye:dark_grey", + "dye:green", + "dye:grey", + "dye:magenta", + "dye:orange", + "dye:pink", + "dye:red", + "dye:violet", + "dye:white", + "dye:yellow", + + "wool:black", + "wool:blue", + "wool:brown", + "wool:cyan", + "wool:dark_green", + "wool:dark_grey", + "wool:green", + "wool:grey", + "wool:magenta", + "wool:orange", + "wool:pink", + "wool:red", + "wool:violet", + "wool:white", + "wool:yellow", + + "unified_inventory:bag_large", + "unified_inventory:bag_medium", + "unified_inventory:bag_small", +--]] + +--[[ LIST UNCATEGORIZED AFTER LOAD +minetest.register_on_mods_loaded(function() + minetest.after(1, function ( ) + local l = {} + for name,_ in pairs(minetest.registered_items) do + if not unified_inventory.find_category(name) then + -- minetest.log("error", minetest.serialize(minetest.registered_items[name])) + table.insert(l, name) + end + end + table.sort(l) + minetest.log(table.concat(l, '",'.."\n"..'"')) + end) +end) +--]] \ No newline at end of file diff --git a/doc/mod_api.txt b/doc/mod_api.txt index 0d100a0..ff52792 100644 --- a/doc/mod_api.txt +++ b/doc/mod_api.txt @@ -101,3 +101,72 @@ Register a non-standard craft recipe: -- ^ Same as `minetest.register_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("category_name", { + symbol = "mod_name:item_name" or "texture.png", + label = "Human Readable Label", + index = 5, + items = { + "mod_name:item_name", + "another_mod:different_item" + } + }) + +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" + + unified_inventory.set_category_symbol("category_name", "mod_name:item_name" or "texture.png") + +Add / override the human readable label for a category: + If unset this will default to the category name + + unified_inventory.set_category_label("category_name", "Human Readable Label") + +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/init.lua b/init.lua index ef42533..391eb3c 100644 --- a/init.lua +++ b/init.lua @@ -10,6 +10,8 @@ unified_inventory = { alternate = {}, current_page = {}, current_searchbox = {}, + current_category = {}, + current_category_scroll = {}, current_index = {}, current_item = {}, current_craft_direction = {}, @@ -33,6 +35,9 @@ unified_inventory = { -- "Lite" mode lite_mode = minetest.settings:get_bool("unified_inventory_lite"), + -- Items automatically added to categories based on item definitions + automatic_categorization = (minetest.settings:get_bool("unified_inventory_automatic_categorization") ~= false), + -- Trash enabled trash_enabled = (minetest.settings:get_bool("unified_inventory_trash") ~= false), imgscale = 1.25, @@ -52,9 +57,9 @@ ui.style_full = { formw = 17.75, formh = 12.25, pagecols = 8, - pagerows = 10, + pagerows = 9, page_x = 10.75, - page_y = 1.45, + page_y = 2.30, craft_x = 2.8, craft_y = 1.15, craftresult_x = 7.8, @@ -85,9 +90,9 @@ ui.style_lite = { formw = 14, formh = 9.75, pagecols = 4, - pagerows = 6, + pagerows = 5, page_x = 10.5, - page_y = 1.25, + page_y = 2.15, craft_x = 2.6, craft_y = 0.75, craftresult_x = 5.75, @@ -100,9 +105,9 @@ ui.style_lite = { craft_guide_resultstr_y = 0.35, give_btn_x = 0.15, main_button_x = 10.5, - main_button_y = 7.9, + main_button_y = 8.15, page_buttons_x = 10.5, - page_buttons_y = 6.3, + page_buttons_y = 6.15, searchwidth = 1.6, form_header_x = 0.2, form_header_y = 0.2, @@ -149,6 +154,8 @@ if sfinv then end dofile(modpath.."/group.lua") +dofile(modpath.."/category.lua") +dofile(modpath.."/default-categories.lua") dofile(modpath.."/internal.lua") dofile(modpath.."/callbacks.lua") dofile(modpath.."/match_craft.lua") diff --git a/internal.lua b/internal.lua index 215a4f5..6113300 100644 --- a/internal.lua +++ b/internal.lua @@ -25,6 +25,20 @@ function ui.get_per_player_formspec(player_name) return table.copy(draw_lite_mode and ui.style_lite or ui.style_full), draw_lite_mode end +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' + 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 + function ui.get_formspec(player, page) if not player then @@ -107,6 +121,48 @@ function ui.get_formspec(player, page) return table.concat(formspec, "") end + -- Category filters + + local categories_pos = { ui_peruser.page_x, ui_peruser.page_y-ui_peruser.btn_spc-0.5 } + local categories_scroll_pos = { ui_peruser.page_x, ui_peruser.form_header_y-(draw_lite_mode and 0 or 0.2) } + + formspec[n] = string.format("background9[%f,%f;%f,%f;%s;false;3]", + ui_peruser.page_x-0.1, categories_scroll_pos[2], + (ui_peruser.btn_spc * ui_peruser.pagecols) + 0.13, 1.4+(draw_lite_mode and 0 or 0.2), + "ui_smallbg_9_sliced.png") + n = n + 1 + + formspec[n] = string.format("label[%f,%f;%s]", ui_peruser.page_x, ui_peruser.form_header_y+(draw_lite_mode and 0.3 or 0.2), "Category:") + n = n + 1 + + local scroll_offset = 0 + local category_count = #unified_inventory.category_list + if category_count > ui_peruser.pagecols then + scroll_offset = unified_inventory.current_category_scroll[player_name] + end + + for index, category in ipairs(unified_inventory.category_list) do + local column = index - scroll_offset + if column > 0 and column <= ui_peruser.pagecols then + local scale = 0.8 + if unified_inventory.current_category[player_name] == category.name then + scale = 1 + end + formspec[n] = formspec_button(ui_peruser, "category_"..category.name, category.symbol, categories_pos, {column-1, 0}, scale, category.label) + n = n + 1 + end + end + if category_count > ui_peruser.pagecols and scroll_offset > 0 then + -- prev + formspec[n] = formspec_button(ui_peruser, "prev_category", "ui_left_icon.png", categories_scroll_pos, {ui_peruser.pagecols - 2, 0}, 0.8, S("Scroll categories left")) + n = n + 1 + end + if category_count > ui_peruser.pagecols and category_count - scroll_offset > ui_peruser.pagecols then + -- next + formspec[n] = formspec_button(ui_peruser, "next_category", "ui_right_icon.png", categories_scroll_pos, {ui_peruser.pagecols - 1, 0}, 0.8, S("Scroll categories right")) + n = n + 1 + end + -- Search box formspec[n] = "field_close_on_enter[searchbox;false]" @@ -205,16 +261,16 @@ function ui.get_formspec(player, page) end end formspec[n] = string.format("label[%f,%f;%s: %s]", - ui_peruser.page_x, ui_peruser.form_header_y, + ui_peruser.page_buttons_x + ui_peruser.btn_spc * (draw_lite_mode and 1 or 2), + ui_peruser.page_buttons_y + 0.1 + ui_peruser.btn_spc * 2, F(S("Page")), S("@1 of @2",page2,pagemax)) end n= n+1 if ui.activefilter[player_name] ~= "" then - formspec[n] = string.format("label[%f,%f;%s:]", - ui_peruser.page_x, ui_peruser.page_y - 0.65, F(S("Filter"))) - formspec[n+1] = string.format("label[%f,%f;%s]", - ui_peruser.page_x, ui_peruser.page_y - 0.25, F(ui.activefilter[player_name])) + formspec[n] = 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 return table.concat(formspec, "") end @@ -225,6 +281,13 @@ function ui.set_inventory_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 @@ -256,13 +319,30 @@ function ui.apply_filter(player, filter, search_dir) end end ui.filtered_items_list[player_name]={} - 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 ~= "" - and ffilter(name, def) then - table.insert(ui.filtered_items_list[player_name], name) + 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) + and ffilter(name, def) then + table.insert(ui.filtered_items_list[player_name], 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) + and ffilter(name, def) then + table.insert(ui.filtered_items_list[player_name], name) + end + end + else + 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 ffilter(name, def) then + table.insert(ui.filtered_items_list[player_name], name) + end end end table.sort(ui.filtered_items_list[player_name]) diff --git a/settingtypes.txt b/settingtypes.txt index 910989f..27768ac 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -9,3 +9,6 @@ unified_inventory_bags (Enable bags) bool true #If enabled, the trash slot can be used by those without both creative #and the give privilege. unified_inventory_trash (Enable trash) bool true + + +unified_inventory_automatic_categorization (Items automatically added to categories) bool true \ No newline at end of file diff --git a/textures/ui_category_all.png b/textures/ui_category_all.png new file mode 100644 index 0000000000000000000000000000000000000000..0af7a53713d99fe343de955bb6cda70ede76a6e3 GIT binary patch literal 1233 zcmV;?1TOoDP)C0001!P)t-s0000* zMMVGr08>*_J3T&!mxv>kdCn{N(>!*Rkd8B^d#sl*99lt{e5&(cT|cr1xzf#0R@o`6>Z?+a8u94Fue zvIu{t+rzkQ=P`lQ@K zpz;*``gc$le12BrUtW-#>yvUDfrujxmjc9TJQIjGRDhoEaIg`mMjVegG66!4=K@%F zIN+$~yPe7{1&+D^cu5jCBna$m1p*1pr}t_QqzYg?&BuBzr}@aO1QeMB81cE4K&=+k z6zKvrb^>zqI#vR}yr2?bIm5{91%5H)i7JPHnS<0kzW^!F`0E5d&40&L9m)#HKTRXL z#0fZouPJcz528#pTL?XGHe=U^668e+h)@$~Mi}eiN(%kF`vRWRe3m!Q#IBbLw)=u5 zk=XNT{$<`|#H=6mMfhY_%a=f}*?{J={Pk6h@AWR$V_Q%e^xx=@z)D~?UXI5w&;>A` z3lOLAOn|_Ad6G>npUuXSCB3woE$DYhZv+z{XncfT%V|En+X-xDP%s2&IOcN!(9;Ff zd?K`}G^BV?F-z#zzH~k|3={EA4IBt zhhcmzm#rl8kLw$atM3ho#?!g6WAa(^g>LqTu>;xyhufc1GI zuIG!8<-R|Z2zp)Sod}G#6~G)Wkequy6m$X5wG`le9(6h`_X#*4&naN``PAI=e2`Nh zsn4h7gjDc6N#A>pBE-?+fzeovzy#xP5^WZ~~uB;N~Ah z-%sV+tl6NH=ZM0H8wsHL1B>T{ihGVIWbppKBI52NEQrFMBWktO_;#Ds@)a=dBP;;& z+|LoUh|_p3FrTa@6Bsao>1Z(;!GPtGJr0%vgdEQWs`L3ovZ5EO<;$mQM*d@SLFexW7<{K(O7;fi`Hpo&uIL@(ZAT zONH&$tmRT+y)NoGA|=2t2m?9a%-2ML&k;c&cP-Czzo6Alx}bZG$n6W9KrVs%{)$H_ vJl^lud`jctKELW!D*pX_-Ou50d|Cbin*%0p!3mn+00000NkvXXu0mjf(tSv~ literal 0 HcmV?d00001 diff --git a/textures/ui_category_none.png b/textures/ui_category_none.png new file mode 100644 index 0000000000000000000000000000000000000000..8976fb0eb122a801499fd2dbe009c0449fed1a6e GIT binary patch literal 7966 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 UNozg^(*OVf07*qoM6N<$f(*TUkpKVy literal 0 HcmV?d00001 diff --git a/textures/ui_smallbg_9_sliced.png b/textures/ui_smallbg_9_sliced.png new file mode 100644 index 0000000000000000000000000000000000000000..865e0c965c47dc4e224e5f5bef279e547168d408 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDD3?#L31Vw-pPk>K|E0FH(?QLmksj8}y5%Zf5 zo&VKa{5Uz`Fa!Iv#d8Ar*w|Hf fE>KaKqQb_IlIdwG-SNT~XbOX;tDnm{r-UW|$5SPz literal 0 HcmV?d00001