diff --git a/.luacheckrc b/.luacheckrc index 6ade552..8067e73 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -7,4 +7,12 @@ read_globals = { "sfinv", "sfinv_buttons", "vector", + "string", + "table", + "ItemStack", +} + +globals = { + "craftguide", + "core", } diff --git a/API.md b/API.md new file mode 100644 index 0000000..82c92f4 --- /dev/null +++ b/API.md @@ -0,0 +1,214 @@ +## API + +### Custom recipes + +Custom recipes are nonconventional crafts outside the main crafting grid. +They can be registered in-game dynamically and have a size beyond 3x3 items. + +**Note:** the registration format differs from the default registration format in everything. +The width is automatically calculated depending where you place the commas. Look at the examples attentively. + +#### Registering a custom crafting type (example) + +```Lua +craftguide.register_craft_type("digging", { + description = "Digging", + icon = "default_tool_steelpick.png", +}) +``` + +#### Registering a custom crafting recipe (examples) + +```Lua +craftguide.register_craft({ + type = "digging", + result = "default:cobble 2", + items = {"default:stone"}, +}) +``` + +```Lua +craftguide.register_craft({ + result = "default:cobble 16", + items = { + "default:stone, default:stone, default:stone", + "default:stone, , default:stone", + "default:stone, default:stone, default:stone", + } +}) +``` + +Recipes can be registered in a Minecraft-like way: + +```Lua +craftguide.register_craft({ + grid = { + "X #", + " ## ", + "X#X#", + "X X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +}) +``` + +Multiples recipes can also be registered: + +```Lua +craftguide.register_craft({ + { + result = "default:mese", + items = { + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + } + }, + + big = { + result = "default:mese 4", + items = { + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + } + }, +}) +``` + +Recipes can be registered from a given URL containing a JSON file (HTTP support is required¹): + +```Lua +craftguide.register_craft({ + url = "https://raw.githubusercontent.com/minetest-mods/craftguide/master/test.json" +}) +``` + +--- + +### Recipe filters + +Recipe filters can be used to filter the recipes shown to players. Progressive +mode is implemented as a recipe filter. + +#### `craftguide.add_recipe_filter(name, function(recipes, player))` + +Adds a recipe filter with the given name. The filter function should return the +recipes to be displayed, given the available recipes and an `ObjectRef` to the +user. Each recipe is a table of the form returned by +`minetest.get_craft_recipe`. + +Example function to hide recipes for items from a mod called "secretstuff": + +```lua +craftguide.add_recipe_filter("Hide secretstuff", function(recipes) + local filtered = {} + for _, recipe in ipairs(recipes) do + if recipe.output:sub(1,12) ~= "secretstuff:" then + filtered[#filtered + 1] = recipe + end + end + + return filtered +end) +``` + +#### `craftguide.set_recipe_filter(name, function(recipe, player))` + +Removes all recipe filters and adds a new one. + +#### `craftguide.remove_recipe_filter(name)` + +Removes the recipe filter with the given name. + +#### `craftguide.get_recipe_filters()` + +Returns a map of recipe filters, indexed by name. + +--- + +### Search filters + +Search filters are used to perform specific searches inside the search field. +They can be used like so: `+=,,<...>` + +Examples: + +- `+groups=cracky,crumbly`: search for groups `cracky` and `crumbly` in all items. +- `sand+groups=falling_node`: search for group `falling_node` for items which contain `sand` in their names. + +Notes: +- If `optional name` is omitted, the search filter will apply to all items, without pre-filtering. +- Filters can be combined. +- The `groups` filter is currently implemented by default. + +#### `craftguide.add_search_filter(name, function(item, values))` + +Adds a search filter with the given name. +The search function should return a boolean value (whether the given item should be listed or not). + +Example function to show items which contain at least a recipe of given width(s): + +```lua +craftguide.add_search_filter("widths", function(item, widths) + local has_width + local recipes = recipes_cache[item] + + if recipes then + for i = 1, #recipes do + local recipe_width = recipes[i].width + for j = 1, #widths do + local width = tonumber(widths[j]) + if width == recipe_width then + has_width = true + break + end + end + end + end + + return has_width +end) +``` + +#### `craftguide.remove_search_filter(name)` + +Removes the search filter with the given name. + +#### `craftguide.get_search_filters()` + +Returns a map of search filters, indexed by name. + +--- + +### Miscellaneous + +#### `craftguide.show(player_name, item, show_usages)` + +Opens the Crafting Guide with the current filter applied. + + * `player_name`: string param. + * `item`: optional, string param. If set, this item is pre-selected. If the item does not exist or has no recipe, use the player's previous selection. By default, player's previous selection is used + * `show_usages`: optional, boolean param. If true, show item usages. + +#### `craftguide.group_stereotypes` + +This is the table indexing the item groups by stereotypes. +You can add a stereotype like so: + +```Lua +craftguide.group_stereotypes.radioactive = "mod:item" +``` + +#### `craftguide.export_url` + +If set, the mod will export all the cached recipes and usages in a JSON format +to the given URL (HTTP support is required¹). + +--- + +**¹** Add `craftguide` to the `secure.http_mods` or `secure.trusted_mods` setting in `minetest.conf`. diff --git a/README.md b/README.md index 00e6054..f1d2e9e 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,22 @@ -## ![Preview1](http://i.imgur.com/fIPNYkb.png) Crafting Guide ## +# ![Preview1](http://i.imgur.com/fIPNYkb.png) Crafting Guide -#### `craftguide` is the most comprehensive crafting guide on Minetest. #### -#### Consult the [Minetest Wiki](http://wiki.minetest.net/Crafting_guide) for more details. #### +#### `craftguide` is the most comprehensive crafting guide on Minetest. +#### Consult the [Minetest Wiki](http://wiki.minetest.net/Crafting_guide) for more details. This crafting guide is a blue book named *"Crafting Guide"* or a wooden sign. This crafting guide features a **progressive mode**. -The progressive mode is a Terraria-like system that only shows recipes you can craft from items in inventory. -The progressive mode can be enabled with `craftguide_progressive_mode = true` in `minetest.conf`. +This mode is a Terraria-like system that shows recipes you can craft +from items you ever had in your inventory. To enable it: `craftguide_progressive_mode = true` in `minetest.conf`. -`craftguide` is also integrated in `sfinv` (Minetest Game inventory) when you enable it with +`craftguide` is also integrated in `sfinv` (Minetest Game inventory). To enable it: `craftguide_sfinv_only = true` in `minetest.conf`. Use the command `/craft` to show the recipe(s) of the pointed node. ---- +For developers, `craftguide` also has a [modding API](https://github.com/minetest-mods/craftguide/blob/master/API.md). -`craftguide` has an API to register **custom recipes**. Demos: -#### Registering a custom crafting type #### -```Lua -craftguide.register_craft_type("digging", { - description = "Digging", - icon = "default_tool_steelpick.png", -}) -``` +Love this mod? Donations are appreciated: https://www.paypal.me/jpg84240 -#### Registering a custom crafting recipe #### -```Lua -craftguide.register_craft({ - type = "digging", - width = 1, - output = "default:cobble 2", - items = {"default:stone"}, -}) -``` -![Preview2](https://i.imgur.com/bToFH38.png) +![Preview2](https://i.imgur.com/w7KMS9G.png) diff --git a/depends.txt b/depends.txt deleted file mode 100644 index f91ea34..0000000 --- a/depends.txt +++ /dev/null @@ -1,3 +0,0 @@ -sfinv? -sfinv_buttons? -intllib? diff --git a/description.txt b/description.txt deleted file mode 100644 index b5c0540..0000000 --- a/description.txt +++ /dev/null @@ -1,2 +0,0 @@ -The most comprehensive Crafting Guide -on Minetest. diff --git a/init.lua b/init.lua index 3508dda..24185f8 100644 --- a/init.lua +++ b/init.lua @@ -1,596 +1,1534 @@ -craftguide = { - custom_crafts = {}, - craft_types = {}, +craftguide = {} + +local p = 0 +local CORE_VERSION = core.get_version().string:match("[^%-]*"):gsub("%.", function(a) + p = p + 1 + return p == 3 and a or "" +end) +CORE_VERSION = tonumber(CORE_VERSION) + +-- Caches +local pdata = {} +local init_items = {} +local searches = {} +local recipes_cache = {} +local usages_cache = {} +local fuel_cache = {} + +local toolrepair + +local progressive_mode = core.settings:get_bool("craftguide_progressive_mode") +local sfinv_only = core.settings:get_bool("craftguide_sfinv_only") and rawget(_G, "sfinv") + +local http = core.request_http_api() + +local reg_items = core.registered_items +local reg_tools = core.registered_tools +local reg_aliases = core.registered_aliases + +local log = core.log +local after = core.after +local clr = core.colorize +local parse_json = core.parse_json +local write_json = core.write_json +local chat_send = core.chat_send_player +local show_formspec = core.show_formspec +local globalstep = core.register_globalstep +local on_shutdown = core.register_on_shutdown +local get_players = core.get_connected_players +local get_craft_result = core.get_craft_result +local on_joinplayer = core.register_on_joinplayer +local get_all_recipes = core.get_all_craft_recipes +local register_command = core.register_chatcommand +local get_player_by_name = core.get_player_by_name +local slz, dslz = core.serialize, core.deserialize +local on_mods_loaded = core.register_on_mods_loaded +local on_leaveplayer = core.register_on_leaveplayer +local on_receive_fields = core.register_on_player_receive_fields + +local ESC = core.formspec_escape +local S = CORE_VERSION >= 500 and core.get_translator("craftguide") or + function(...) + local args, i = {...}, 1 + + return args[1]:gsub("@%d+", function() + i = i + 1 + return args[i] + end) + end + +local ES = function(...) + return ESC(S(...)) +end + +local maxn, sort, concat, copy, insert, remove = + table.maxn, table.sort, table.concat, table.copy, + table.insert, table.remove + +local fmt, find, gmatch, match, sub, split, upper, lower = + string.format, string.find, string.gmatch, string.match, + string.sub, string.split, string.upper, string.lower + +local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil +local pairs, next, type, tostring, io = pairs, next, type, tostring, io +local vec_add, vec_mul = vector.add, vector.multiply + +local ROWS = 9 +local LINES = sfinv_only and 5 or 9 +local IPP = ROWS * LINES +local WH_LIMIT = 8 + +local XOFFSET = sfinv_only and 3.83 or 11.2 +local YOFFSET = sfinv_only and 4.9 or 1 + +local PNG = { + bg = "craftguide_bg.png", + bg_full = "craftguide_bg_full.png", + search = "craftguide_search_icon.png", + clear = "craftguide_clear_icon.png", + prev = "craftguide_next_icon.png^\\[transformFX", + next = "craftguide_next_icon.png", + arrow = "craftguide_arrow.png", + fire = "craftguide_fire.png", + book = "craftguide_book.png", + sign = "craftguide_sign.png", + selected = "craftguide_selected.png", + + search_hover = "craftguide_search_icon_hover.png", + clear_hover = "craftguide_clear_icon_hover.png", + prev_hover = "craftguide_next_icon_hover.png^\\[transformFX", + next_hover = "craftguide_next_icon_hover.png", } -local mt = minetest -local datas = {searches = {}} +local FMT = { + box = "box[%f,%f;%f,%f;%s]", + label = "label[%f,%f;%s]", + image = "image[%f,%f;%f,%f;%s]", + button = "button[%f,%f;%f,%f;%s;%s]", + tooltip = "tooltip[%f,%f;%f,%f;%s]", + item_image = "item_image[%f,%f;%f,%f;%s]", + image_button = "image_button[%f,%f;%f,%f;%s;%s;%s]", + item_image_button = "item_image_button[%f,%f;%f,%f;%s;%s;%s]", + arrow = "image_button[%f,%f;0.8,0.8;%s;%s;;;false;%s]", +} -local progressive_mode = mt.settings:get_bool("craftguide_progressive_mode") -local sfinv_only = mt.settings:get_bool("craftguide_sfinv_only") +local function mul_elem(elem, n) + local fstr, elems = "", {} -local get_recipe, get_recipes = mt.get_craft_recipe, mt.get_all_craft_recipes -local get_result, show_formspec = mt.get_craft_result, mt.show_formspec -local reg_items = mt.registered_items + for i = 1, n do + fstr = fstr .. "%s" + elems[i] = elem + end -craftguide.path = mt.get_modpath("craftguide") + return fmt(fstr, unpack(elems)) +end --- Intllib -local S = dofile(craftguide.path .. "/intllib.lua") -craftguide.intllib = S - --- Lua 5.3 removed `table.maxn`, use this alternative in case of breakage: --- https://github.com/kilbith/xdecor/blob/master/handlers/helpers.lua#L1 -local remove, maxn, sort, concat = table.remove, table.maxn, table.sort, table.concat -local vector_add, vector_mul = vector.add, vector.multiply -local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil - -local DEFAULT_SIZE = 10 -local MIN_LIMIT, MAX_LIMIT = 10, 12 -DEFAULT_SIZE = min(MAX_LIMIT, max(MIN_LIMIT, DEFAULT_SIZE)) - -local GRID_LIMIT = 5 -local BUTTON_SIZE = 1.1 - -local group_stereotypes = { - wool = "wool:white", - dye = "dye:white", +craftguide.group_stereotypes = { + dye = "dye:white", + wool = "wool:white", + coal = "default:coal_lump", + vessel = "vessels:glass_bottle", + flower = "flowers:dandelion_yellow", water_bucket = "bucket:bucket_water", - vessel = "vessels:glass_bottle", - coal = "default:coal_lump", - flower = "flowers:dandelion_yellow", mesecon_conductor_craftable = "mesecons:wire_00000000_off", } -local function extract_groups(str) - if str:sub(1,6) ~= "group:" then return end - return str:sub(7):split(",") +local function err(str) + return log("error", str) end -function craftguide.register_craft_type(name, def) - if not craftguide.craft_types[name] then - craftguide.craft_types[name] = def +local function msg(name, str) + return chat_send(name, fmt("[craftguide] %s", clr("#f00", str))) +end + +local function is_str(x) + return type(x) == "string" +end + +local function true_str(str) + return is_str(str) and str ~= "" +end + +local function is_table(x) + return type(x) == "table" +end + +local function is_func(x) + return type(x) == "function" +end + +local function is_group(item) + return sub(item, 1, 6) == "group:" +end + +local function clean_name(item) + if sub(item, 1, 1) == ":" then + item = sub(item, 2) end + + return item end -craftguide.register_craft_type("digging", { - description = S("Digging"), - icon = "default_tool_steelpick.png", -}) +local function array_diff(t1, t2) + local hash = {} -function craftguide.register_craft(def) - craftguide.custom_crafts[#craftguide.custom_crafts + 1] = def -end - -craftguide.register_craft({ - type = "digging", - width = 1, - output = "default:cobble", - items = {"default:stone"}, -}) - -local function colorize(str) - return mt.colorize("#FFFF00", str) -end - -local function get_fueltime(item) - return get_result({method = "fuel", width = 1, items = {item}}).time -end - -local function reset_datas(data) - data.show_usage = nil - data.filter = "" - data.item = nil - data.pagenum = 1 - data.rnum = 1 - data.fuel = nil -end - -local function in_table(T) - for i = 1, #T do - if T[i] then - return true - end + for i = 1, #t1 do + local v = t1[i] + hash[v] = true end -end -local function group_to_items(group) - local items_with_group, counter = {}, 0 - for name, def in pairs(reg_items) do - if def.groups[group:sub(7)] then - counter = counter + 1 - items_with_group[counter] = name + for i = 1, #t2 do + local v = t2[i] + hash[v] = nil + end + + local diff, c = {}, 0 + + for i = 1, #t1 do + local v = t1[i] + if hash[v] then + c = c + 1 + diff[c] = v end end - return items_with_group + return diff end -local function item_in_inv(inv, item) - return inv:contains_item("main", item) -end +local function table_eq(T1, T2) + local avoid_loops = {} -local function group_to_item(item) - if item:sub(1,6) == "group:" then - local itemsub = item:sub(7) - if group_stereotypes[itemsub] then - item = group_stereotypes[itemsub] - elseif reg_items["default:" .. itemsub] then - item = item:gsub("group:", "default:") - else - for name, def in pairs(reg_items) do - if def.groups[item:match("[^,:]+$")] then - item = name - end - end - end - end - - return item:sub(1,6) == "group:" and "" or item -end - -local function get_tooltip(item, recipe_type, cooktime, groups) - local tooltip, item_desc = "tooltip[" .. item .. ";", "" - local fueltime = get_fueltime(item) - local has_extras = groups or recipe_type == "cooking" or fueltime > 0 - - if reg_items[item] then - if not groups then - item_desc = reg_items[item].description - end - else - return tooltip .. S("Unknown Item (@1)", item) .. "]" - end - - if groups then - local groupstr = "" - for i = 1, #groups do - groupstr = groupstr .. - colorize(groups[i]) .. (groups[i + 1] and ", " or "") + local function recurse(t1, t2) + if type(t1) ~= type(t2) then return end + if not is_table(t1) then + return t1 == t2 end - tooltip = tooltip .. - S("Any item belonging to the group(s)") .. ": " .. groupstr - end + if avoid_loops[t1] then + return avoid_loops[t1] == t2 + end - if recipe_type == "cooking" then - tooltip = tooltip .. item_desc .. "\n" .. - S("Cooking time") .. ": " .. colorize(cooktime) - end + avoid_loops[t1] = t2 + local t2k, t2kv = {}, {} - if fueltime > 0 then - tooltip = tooltip .. item_desc .. "\n" .. - S("Burning time") .. ": " .. colorize(fueltime) - end - - return has_extras and tooltip .. "]" or "" -end - -local function _get_recipe(iX, iY, xoffset, recipe_num, recipes, show_usage) - local fs, recipes_total = {}, #recipes - if recipes_total > 1 then - fs[#fs + 1] = "button[" .. (iX - (sfinv_only and 2.2 or 2.6)) .. "," .. - (iY + (sfinv_only and 3.9 or 3.3)) .. ";2.2,1;alternate;" .. - (show_usage and S("Usage") or S("Recipe")) .. " " .. - S("@1 of @2", recipe_num, recipes_total) .. "]" - end - - local recipe_type = recipes[recipe_num].type - local items = recipes[recipe_num].items - local width = recipes[recipe_num].width - - local cooktime = width - if recipe_type == "cooking" then - width = 1 - elseif width == 0 then - width = min(3, #items) - end - - local rows = ceil(maxn(items) / width) - local rightest, s_btn_size = 0 - - if recipe_type ~= "cooking" and (width > GRID_LIMIT or rows > GRID_LIMIT) then - fs[#fs + 1] = "label[" .. ((iX / 2) - 2) .. "," .. (iY + 2.2) .. ";" .. - S("Recipe is too big to be displayed (@1x@2)", width, rows) .. "]" - - return concat(fs) - else - for i, v in pairs(items) do - local X = ceil((i - 1) % width + xoffset - width) - - (sfinv_only and 0 or 0.2) - local Y = ceil(i / width + (iY + 2) - min(2, rows)) - - if recipe_type ~= "cooking" and (width > 3 or rows > 3) then - BUTTON_SIZE = width > 3 and 3 / width or 3 / rows - s_btn_size = BUTTON_SIZE - X = BUTTON_SIZE * (i % width) + xoffset - 2.65 - Y = BUTTON_SIZE * floor((i - 1) / width) + (iY + 3) - min(2, rows) + for k in pairs(t2) do + if is_table(k) then + insert(t2kv, k) end - if X > rightest then - rightest = X - end - - local groups = extract_groups(v) - local label = groups and "\nG" or "" - local item_r = group_to_item(v) - local tltip = get_tooltip(item_r, recipe_type, cooktime, groups) - - fs[#fs + 1] = "item_image_button[" .. X .. "," .. - (Y + (sfinv_only and 0.7 or 0.2)) .. ";" .. - BUTTON_SIZE .. "," .. BUTTON_SIZE .. ";" .. item_r .. - ";" .. item_r:match("%S*") .. ";" .. label .. "]" .. tltip + t2k[k] = true end - BUTTON_SIZE = 1.1 - end - - local custom_recipe = craftguide.craft_types[recipe_type] - if recipe_type == "cooking" or (recipe_type == "normal" and width == 0) or - custom_recipe then - local icon = recipe_type == "cooking" and "furnace" or "shapeless" - local coords = (rightest + 1.2) .. "," .. - (iY + (sfinv_only and 2.2 or 1.7)) .. - ";0.5,0.5;" - - fs[#fs + 1] = "image[" .. coords .. - (custom_recipe and custom_recipe.icon or - "craftguide_" .. icon .. ".png^[resize:16x16") .. "]" - - fs[#fs + 1] = "tooltip[" .. coords .. - (custom_recipe and custom_recipe.description or - recipe_type:gsub("^%l", string.upper)) .. "]" - end - - local output = recipes[recipe_num].output - local output_s = output:match("%S+") - local output_is_fuel = get_fueltime(output) > 0 - - local arrow_X = rightest + (s_btn_size or BUTTON_SIZE) - local output_X = arrow_X + 0.9 - - fs[#fs + 1] = "image[" .. arrow_X .. "," .. - (iY + (sfinv_only and 2.85 or 2.35)) .. - ";0.9,0.7;craftguide_arrow.png]" - - fs[#fs + 1] = "item_image_button[" .. output_X .. "," .. - (iY + (sfinv_only and 2.7 or 2.2)) .. ";" .. - BUTTON_SIZE .. "," .. BUTTON_SIZE .. ";" .. - output .. ";" .. output_s .. ";]" - - fs[#fs + 1] = get_tooltip(output_s) - - if output_is_fuel then - fs[#fs + 1] = "image[" .. (output_X + 1) .. "," .. - (iY + (sfinv_only and 2.83 or 2.33)) .. - ";0.6,0.4;craftguide_arrow.png]" - - fs[#fs + 1] = "image[" .. (output_X + 1.6) .. "," .. - (iY + (sfinv_only and 2.68 or 2.18)) .. - ";0.6,0.6;craftguide_fire.png]" - end - - return concat(fs) -end - -local function get_formspec(player_name) - local data = datas[player_name] - local iY = sfinv_only and 4 or data.iX - 5 - local ipp = data.iX * iY - - if not data.items then - data.items = datas.init_items - end - - data.pagemax = max(1, ceil(#data.items / ipp)) - - local fs = {} - if not sfinv_only then - fs[#fs + 1] = "size[" .. (data.iX - 0.35) .. "," .. (iY + 4) .. ";]" - fs[#fs + 1] = "background[1,1;1,1;craftguide_bg.png;true]" - fs[#fs + 1] = "tooltip[size_inc;" .. S("Increase window size") .. "]" - fs[#fs + 1] = "tooltip[size_dec;" .. S("Decrease window size") .. "]" - fs[#fs + 1] = "image_button[" .. (data.iX * 0.47) .. - ",0.12;0.8,0.8;craftguide_zoomin_icon.png;size_inc;]" - fs[#fs + 1] = "image_button[" .. ((data.iX * 0.47) + 0.6) .. - ",0.12;0.8,0.8;craftguide_zoomout_icon.png;size_dec;]" - end - - fs[#fs + 1] = [[ - image_button[2.4,0.12;0.8,0.8;craftguide_search_icon.png;search;] - image_button[3.05,0.12;0.8,0.8;craftguide_clear_icon.png;clear;] - field_close_on_enter[filter;false] - ]] - - fs[#fs + 1] = "tooltip[search;" .. S("Search") .. "]" - fs[#fs + 1] = "tooltip[clear;" .. S("Reset") .. "]" - fs[#fs + 1] = "tooltip[prev;" .. S("Previous page") .. "]" - fs[#fs + 1] = "tooltip[next;" .. S("Next page") .. "]" - fs[#fs + 1] = "image_button[" .. (data.iX - (sfinv_only and 2.6 or 3.1)) .. - ",0.12;0.8,0.8;craftguide_prev_icon.png;prev;]" - fs[#fs + 1] = "label[" .. (data.iX - (sfinv_only and 1.7 or 2.2)) .. ",0.22;" .. - colorize(data.pagenum) .. " / " .. data.pagemax .. "]" - fs[#fs + 1] = "image_button[" .. (data.iX - (sfinv_only and 0.7 or 1.2) - - (data.iX >= 11 and 0.08 or 0)) .. - ",0.12;0.8,0.8;craftguide_next_icon.png;next;]" - fs[#fs + 1] = "field[0.3,0.32;2.5,1;filter;;" .. mt.formspec_escape(data.filter) .. "]" - - local xoffset = data.iX / 2.15 - - if not next(data.items) then - fs[#fs + 1] = "label[" .. ((data.iX / 2) - 1) .. - ",2;" .. S("No item to show") .. "]" - end - - local first_item = (data.pagenum - 1) * ipp - for i = first_item, first_item + ipp - 1 do - local name = data.items[i + 1] - if not name then break end - local X = i % data.iX - local Y = (i % ipp - X) / data.iX + 1 - - fs[#fs + 1] = "item_image_button[" .. - (X - (sfinv_only and 0 or (X * 0.05))) .. "," .. - Y .. ";" .. BUTTON_SIZE .. "," .. BUTTON_SIZE .. ";" .. - name .. ";" .. name .. "_inv;]" - end - - if data.item and reg_items[data.item] then - if not data.recipes_item or (data.fuel and not get_recipe(data.item).items) then - local X = floor(xoffset) - (sfinv_only and 0 or 0.2) - - fs[#fs + 1] = "item_image_button[" .. X .. "," .. - (iY + (sfinv_only and 2.7 or 2.2)) .. - ";" .. BUTTON_SIZE .. "," .. BUTTON_SIZE .. - ";" .. data.item .. ";" .. data.item .. ";]" - - fs[#fs + 1] = "image[" .. (X + 1.1) .. "," .. - (iY + (sfinv_only and 2.85 or 2.35)) .. - ";0.9,0.7;craftguide_arrow.png]" - - fs[#fs + 1] = get_tooltip(data.item) - - fs[#fs + 1] = "image[" .. (X + 2.1) .. "," .. - (iY + (sfinv_only and 2.68 or 2.18)) .. - ";1.1,1.1;craftguide_fire.png]" - else - local usage = data.show_usage - fs[#fs + 1] = _get_recipe(data.iX, - iY, - xoffset, - data.rnum, - (usage and data.usages or data.recipes_item), - usage) - end - end - - fs = concat(fs) - data.formspec = fs - - if sfinv_only then - return fs - else - show_formspec(player_name, "craftguide", fs) - end -end - -local show_fs = function(player, player_name) - if sfinv_only then - local context = sfinv.get_or_create_context(player) - sfinv.set_player_inventory_formspec(player, context) - else - get_formspec(player_name) - end -end - -local function recipe_in_inv(inv, item_name, recipes_f) - local recipes = recipes_f or get_recipes(item_name) or {} - local show_item_recipes = {} - - for i = 1, #recipes do - show_item_recipes[i] = true - for _, item in pairs(recipes[i].items) do - local group_in_inv = false - if item:sub(1,6) == "group:" then - local groups = group_to_items(item) - for j = 1, #groups do - if item_in_inv(inv, groups[j]) then - group_in_inv = true + for k1, v1 in pairs(t1) do + local v2 = t2[k1] + if type(k1) == "table" then + local ok + for i = 1, #t2kv do + local tk = t2kv[i] + if table_eq(k1, tk) and recurse(v1, t2[tk]) then + remove(t2kv, i) + t2k[tk] = nil + ok = true + break end end - end - if not group_in_inv and not item_in_inv(inv, item) then - show_item_recipes[i] = false + + if not ok then return end + else + if v2 == nil then return end + t2k[k1] = nil + if not recurse(v1, v2) then return end end end + + if next(t2k) then return end + return true end - for i = #show_item_recipes, 1, -1 do - if not show_item_recipes[i] then - remove(recipes, i) - end - end - - return recipes, in_table(show_item_recipes) + return recurse(T1, T2) end -local function get_filter_items(data, player) - local filter = data.filter - if datas.searches[filter] then - data.items = datas.searches[filter] +local function table_merge(t1, t2, hash) + t1 = t1 or {} + t2 = t2 or {} + + if hash then + for k, v in pairs(t2) do + t1[k] = v + end + else + local c = #t1 + + for i = 1, #t2 do + c = c + 1 + t1[c] = t2[i] + end + end + + return t1 +end + +local function table_replace(t, val, new) + for k, v in pairs(t) do + if v == val then + t[k] = new + end + end +end + +local craft_types = {} + +function craftguide.register_craft_type(name, def) + if not true_str(name) then + return err"craftguide.register_craft_type(): name missing" + end + + if not is_str(def.description) then + def.description = "" + end + + if not is_str(def.icon) then + def.icon = "" + end + + craft_types[name] = def +end + +function craftguide.register_craft(def) + local width, c = 0, 0 + + if true_str(def.url) then + if not http then + return err"No HTTP support for this mod. " .. + "Add it to the `secure.http_mods` or `secure.trusted_mods` setting." + end + + http.fetch({url = def.url}, function(result) + if result.succeeded then + local t = parse_json(result.data) + if is_table(t) then + return craftguide.register_craft(t) + end + end + end) + return end - local items_list = progressive_mode and data.init_filter_items or datas.init_items - local inv = player:get_inventory() - local filtered_list, counter = {}, 0 + if not is_table(def) or not next(def) then + return err"craftguide.register_craft(): craft definition missing" + end - for i = 1, #items_list do - local item = items_list[i] - local item_desc = reg_items[item].description:lower() + if #def > 1 then + for _, v in pairs(def) do + craftguide.register_craft(v) + end + return + end - if filter ~= "" then - if item:find(filter, 1, true) or item_desc:find(filter, 1, true) then - counter = counter + 1 - filtered_list[counter] = item - end - elseif progressive_mode then - local _, has_item = recipe_in_inv(inv, item) - if has_item then - counter = counter + 1 - filtered_list[counter] = item + if def.result then + def.output = def.result -- Backward compatibility + def.result = nil + end + + if not true_str(def.output) then + return err"craftguide.register_craft(): output missing" + end + + if not is_table(def.items) then + def.items = {} + end + + if def.grid then + if not is_table(def.grid) then + def.grid = {} + end + + if not is_table(def.key) then + def.key = {} + end + + local cp = copy(def.grid) + sort(cp, function(a, b) + return #a > #b + end) + + width = #cp[1] + + for i = 1, #def.grid do + while #def.grid[i] < width do + def.grid[i] = def.grid[i] .. " " end end - end - if progressive_mode then - if not data.items then - data.init_filter_items = filtered_list + for symbol in gmatch(concat(def.grid), ".") do + c = c + 1 + def.items[c] = def.key[symbol] end - elseif filter ~= "" then - -- Cache the results only if searched 2 times - if datas.searches[filter] == nil then - datas.searches[filter] = false - else - datas.searches[filter] = filtered_list + else + local items, len = def.items, #def.items + def.items = {} + + for i = 1, len do + items[i] = items[i]:gsub(",", ", ") + local rlen = #split(items[i], ",") + + if rlen > width then + width = rlen + end + end + + for i = 1, len do + while #split(items[i], ",") < width do + items[i] = items[i] .. ", " + end + end + + for name in gmatch(concat(items, ","), "[%s%w_:]+") do + c = c + 1 + def.items[c] = match(name, "%S+") end end - data.items = filtered_list + local output = match(def.output, "%S+") + recipes_cache[output] = recipes_cache[output] or {} + + def.custom = true + def.width = width + insert(recipes_cache[output], def) end -local function init_datas(user, name) - datas[name] = {filter = "", pagenum = 1, iX = sfinv_only and 8 or DEFAULT_SIZE} - if progressive_mode then - get_filter_items(datas[name], user) +local recipe_filters = {} + +function craftguide.add_recipe_filter(name, f) + if not true_str(name) then + return err"craftguide.add_recipe_filter(): name missing" + elseif not is_func(f) then + return err"craftguide.add_recipe_filter(): function missing" end + + recipe_filters[name] = f end -local function add_custom_recipes(item, recipes) - for j = 1, #craftguide.custom_crafts do - local craft = craftguide.custom_crafts[j] - if craft.output:match("%S*") == item then - recipes[#recipes + 1] = { - type = craft.type, - width = craft.width, - items = craft.items, - output = craft.output, - } - end +function craftguide.set_recipe_filter(name, f) + if not is_str(name) then + return err"craftguide.set_recipe_filter(): name missing" + elseif not is_func(f) then + return err"craftguide.set_recipe_filter(): function missing" + end + + recipe_filters = {[name] = f} +end + +function craftguide.remove_recipe_filter(name) + recipe_filters[name] = nil +end + +function craftguide.get_recipe_filters() + return recipe_filters +end + +local function apply_recipe_filters(recipes, player) + for _, filter in pairs(recipe_filters) do + recipes = filter(recipes, player) end return recipes end -local function get_init_items() - local items_list, c = {}, 0 - local function list(name) - c = c + 1 - items_list[c] = name +local search_filters = {} + +function craftguide.add_search_filter(name, f) + if not true_str(name) then + return err"craftguide.add_search_filter(): name missing" + elseif not is_func(f) then + return err"craftguide.add_search_filter(): function missing" end - for name, def in pairs(reg_items) do - local is_fuel = get_fueltime(name) > 0 - if (not (def.groups.not_in_craft_guide == 1 or - def.groups.not_in_creative_inventory == 1)) and - (get_recipe(name).items or is_fuel) and - def.description and def.description ~= "" then - list(name) - end - end - - for i = 1, #craftguide.custom_crafts do - local craft = craftguide.custom_crafts[i] - local output = craft.output:match("%S*") - local listed - - for j = 1, #items_list do - local listed_item = items_list[j] - if output == listed_item then - listed = true - break - end - end - - if not listed then - list(output) - end - end - - sort(items_list) - datas.init_items = items_list + search_filters[name] = f end -mt.register_on_mods_loaded(function() - get_init_items() -end) +function craftguide.remove_search_filter(name) + search_filters[name] = nil +end -local function get_item_usages(item) - local usages = {} - for name, def in pairs(reg_items) do - if not (def.groups.not_in_craft_guide == 1 or - def.groups.not_in_creative_inventory == 1) and - get_recipe(name).items and def.description and def.description ~= "" then - local recipes = get_recipes(name) - for i = 1, #recipes do - local recipe = recipes[i] - local items = recipe.items +function craftguide.get_search_filters() + return search_filters +end - for j = 1, #items do - if items[j] == item then - usages[#usages + 1] = { - type = recipe.type, - items = items, - width = recipe.width, - output = recipe.output, - } - break - end - end +local function item_has_groups(item_groups, groups) + for i = 1, #groups do + local group = groups[i] + if (item_groups[group] or 0) == 0 then return end + end + + return true +end + +local function extract_groups(str) + return split(sub(str, 7), ",") +end + +local function item_in_recipe(item, recipe) + local clean_item = reg_aliases[item] or item + for _, recipe_item in pairs(recipe.items) do + local clean_recipe_item = reg_aliases[recipe_item] or recipe_item + if clean_recipe_item == clean_item then + return true + end + end +end + +local function groups_item_in_recipe(item, recipe) + local def = reg_items[item] + if not def then return end + local item_groups = def.groups + + for _, recipe_item in pairs(recipe.items) do + if is_group(recipe_item) then + local groups = extract_groups(recipe_item) + + if item_has_groups(item_groups, groups) then + local usage = copy(recipe) + table_replace(usage.items, recipe_item, item) + return usage end end end +end + +local function get_filtered_items(player, data) + local items, known, c = {}, 0, 0 + + for i = 1, #init_items do + local item = init_items[i] + local recipes = recipes_cache[item] + local usages = usages_cache[item] + + recipes = #apply_recipe_filters(recipes or {}, player) + usages = #apply_recipe_filters(usages or {}, player) + + if recipes > 0 or usages > 0 then + c = c + 1 + items[c] = item + + if data then + known = known + recipes + usages + end + end + end + + if data then + data.known_recipes = known + end + + return items +end + +local function get_usages(item) + local usages, c = {}, 0 + + for _, recipes in pairs(recipes_cache) do + for i = 1, #recipes do + local recipe = recipes[i] + if item_in_recipe(item, recipe) then + c = c + 1 + usages[c] = recipe + else + recipe = groups_item_in_recipe(item, recipe) + if recipe then + c = c + 1 + usages[c] = recipe + end + end + end + end + + if fuel_cache[item] then + usages[#usages + 1] = { + type = "fuel", + items = {item}, + replacements = fuel_cache.replacements[item], + } + end return usages end -local function get_fields(player, ...) - local args, formname, fields = {...} - if sfinv_only then - fields = args[1] - else - formname, fields = args[1], args[2] +local function get_burntime(item) + return get_craft_result({method = "fuel", items = {item}}).time +end + +local function cache_fuel(item) + local burntime = get_burntime(item) + if burntime > 0 then + fuel_cache[item] = burntime + end +end + +local function cache_usages(item) + local usages = get_usages(item) + if #usages > 0 then + usages_cache[item] = table_merge(usages, usages_cache[item] or {}) + end +end + +local function cache_recipes(output) + local recipes = get_all_recipes(output) or {} + if #recipes > 0 then + recipes_cache[output] = recipes + end +end + +local function get_recipes(item, data, player) + local clean_item = reg_aliases[item] or item + local recipes = recipes_cache[clean_item] + local usages = usages_cache[clean_item] + + if recipes then + recipes = apply_recipe_filters(recipes, player) end - if not sfinv_only and formname ~= "craftguide" then return end - local player_name = player:get_player_name() - local data = datas[player_name] + local no_recipes = not recipes or #recipes == 0 + if no_recipes and not usages then + return + elseif sfinv_only and usages and no_recipes then + data.show_usages = true + end - if fields.clear then - reset_datas(data) - data.items = progressive_mode and data.init_filter_items or datas.init_items - show_fs(player, player_name) + if not sfinv_only or (sfinv_only and data.show_usages) then + usages = apply_recipe_filters(usages, player) + end - elseif fields.alternate then - local num - if data.show_usage then - num = data.usages[data.rnum + 1] - else - num = data.recipes_item[data.rnum + 1] + local no_usages = not usages or #usages == 0 + + return not no_recipes and recipes or nil, + not no_usages and usages or nil +end + +local function groups_to_items(groups, get_all) + if not get_all and #groups == 1 then + local group = groups[1] + local def_gr = "default:" .. group + local stereotypes = craftguide.group_stereotypes + local stereotype = stereotypes and stereotypes[group] + + if stereotype then + return stereotype + elseif reg_items[def_gr] then + return def_gr + end + end + + local names = {} + for name, def in pairs(reg_items) do + if item_has_groups(def.groups, groups) then + if get_all then + names[#names + 1] = name + else + return name + end + end + end + + return get_all and names or "" +end + +local function repairable(tool) + local def = reg_tools[tool] + return toolrepair and def and def.groups and def.groups.disable_repair ~= 1 +end + +local function get_desc(name) + local def = reg_items[name] + + return def and (match(def.description, "%)([%w%s]*)") or def.description) or + (def and match(name, ":.*"):gsub("%W%l", upper):sub(2):gsub("_", " ") or + S("Unknown Item (@1)", name)) +end + +local function get_tooltip(name, info) + local tooltip + + if info.groups then + local groupstr, c = {}, 0 + + for i = 1, #info.groups do + c = c + 1 + groupstr[c] = clr("#ff0", info.groups[i]) end - data.rnum = num and data.rnum + 1 or 1 - show_fs(player, player_name) + groupstr = concat(groupstr, ", ") + tooltip = S("Any item belonging to the group(s): @1", groupstr) + else + tooltip = get_desc(name) + end - elseif (fields.key_enter_field == "filter" or fields.search) and - fields.filter ~= "" then - data.filter = fields.filter:lower() + local function add(str) + return fmt("%s\n%s", tooltip, str) + end + + if info.cooktime then + tooltip = add(S("Cooking time: @1", clr("#ff0", info.cooktime))) + end + + if info.burntime then + tooltip = add(S("Burning time: @1", clr("#ff0", info.burntime))) + end + + if info.replace then + local desc = clr("#ff0", get_desc(info.replace)) + + if info.cooktime then + tooltip = add(S("Replaced by @1 on smelting", desc)) + elseif info.burntime then + tooltip = add(S("Replaced by @1 on burning", desc)) + else + tooltip = add(S("Replaced by @1 on crafting", desc)) + end + end + + if info.repair then + tooltip = add(S("Repairable by step of @1", clr("#ff0", toolrepair .. "%"))) + end + + if info.rarity then + local chance = (1 / info.rarity) * 100 + tooltip = add(S("@1 of chance to drop", clr("#ff0", chance .. "%"))) + end + + return fmt("tooltip[%s;%s]", name, ESC(tooltip)) +end + +local function get_output_fs(fs, L) + local custom_recipe = craft_types[L.recipe.type] + + if custom_recipe or L.shapeless or L.recipe.type == "cooking" then + local icon = custom_recipe and custom_recipe.icon or + L.shapeless and "shapeless" or "furnace" + + if not custom_recipe then + icon = fmt("craftguide_%s.png^[resize:16x16", icon) + end + + local pos_x = L.rightest + L.btn_size + 0.1 + local pos_y = YOFFSET + (sfinv_only and 0.25 or -0.45) + + fs[#fs + 1] = fmt(FMT.image, pos_x, pos_y + L.spacing, 0.5, 0.5, icon) + + local tooltip = custom_recipe and custom_recipe.description or + L.shapeless and S"Shapeless" or S"Cooking" + + if CORE_VERSION >= 500 then + fs[#fs + 1] = fmt(FMT.tooltip, pos_x, pos_y + L.spacing, 0.5, 0.5, ESC(tooltip)) + end + end + + local arrow_X = L.rightest + (L._btn_size or 1.1) + local output_X = arrow_X + 0.9 + + fs[#fs + 1] = fmt(FMT.image, + arrow_X, YOFFSET + (sfinv_only and 0.9 or 0.2) + L.spacing, + 0.9, 0.7, PNG.arrow) + + if L.recipe.type == "fuel" then + fs[#fs + 1] = fmt(FMT.image, + output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing, + 1.1, 1.1, PNG.fire) + else + local item = L.recipe.output + item = clean_name(item) + local name = match(item, "%S*") + + if CORE_VERSION >= 510 then + fs[#fs + 1] = fmt(FMT.image, + output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing, + 1.1, 1.1, PNG.selected) + end + + fs[#fs + 1] = fmt(FMT.item_image_button, + output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing, + 1.1, 1.1, item, name, "") + + local infos = { + unknown = not reg_items[name] or nil, + burntime = fuel_cache[name], + repair = repairable(name), + rarity = L.rarity, + } + + if next(infos) then + fs[#fs + 1] = get_tooltip(name, infos) + end + + if infos.burntime then + fs[#fs + 1] = fmt(FMT.image, + output_X + 1, YOFFSET + (sfinv_only and 0.7 or 0.1) + L.spacing, + 0.6, 0.4, PNG.arrow) + + fs[#fs + 1] = fmt(FMT.image, + output_X + 1.6, YOFFSET + (sfinv_only and 0.55 or 0) + L.spacing, + 0.6, 0.6, PNG.fire) + end + end +end + +local function get_grid_fs(fs, rcp, spacing) + local width = rcp.width or 1 + local replacements = rcp.replacements + local rarity = rcp.rarity + local rightest, btn_size, _btn_size = 0, 1.1 + local cooktime, shapeless + + if rcp.type == "cooking" then + cooktime, width = width, 1 + elseif width == 0 and not rcp.custom then + shapeless = true + local n = #rcp.items + width = (n < 5 and n > 1) and 2 or min(3, max(1, n)) + end + + local rows = ceil(maxn(rcp.items) / width) + + if width > WH_LIMIT or rows > WH_LIMIT then + fs[#fs + 1] = fmt(FMT.label, + XOFFSET + (sfinv_only and -1.5 or -1.6), + YOFFSET + (sfinv_only and 0.5 or spacing), + ES("Recipe's too big to be displayed (@1x@2)", width, rows)) + + return concat(fs) + end + + local large_recipe = width > 3 or rows > 3 + + if large_recipe then + fs[#fs + 1] = "style_type[item_image_button;border=true]" + end + + for i = 1, width * rows do + local item = rcp.items[i] or "" + item = clean_name(item) + local name = match(item, "%S*") + + local X = ceil((i - 1) % width - width) + XOFFSET + local Y = ceil(i / width) + YOFFSET - min(2, rows) + spacing + + if large_recipe then + local xof = 1 - 4 / width + local yof = 1 - 4 / rows + local x_y = width > rows and xof or yof + + btn_size = width > rows and + (3.5 + (xof * 2)) / width or (3.5 + (yof * 2)) / rows + _btn_size = btn_size + + X = (btn_size * ((i - 1) % width) + XOFFSET - + (sfinv_only and 2.83 or 0.5)) * (0.83 - (x_y / 5)) + Y = (btn_size * floor((i - 1) / width) + + (sfinv_only and 5.81 or 5.5) + x_y) * (0.86 - (x_y / 5)) + end + + if X > rightest then + rightest = X + end + + local groups + + if is_group(name) then + groups = extract_groups(name) + item = groups_to_items(groups) + end + + local label = groups and "\nG" or "" + local replace + + if replacements then + for j = 1, #replacements do + local replacement = replacements[j] + if replacement[1] == name then + label = (label ~= "" and "\n" or "") .. label .. "\nR" + replace = replacement[2] + end + end + end + + if CORE_VERSION >= 510 and not large_recipe then + fs[#fs + 1] = fmt(FMT.image, + X, Y + (sfinv_only and 0.7 or 0), + btn_size, btn_size, PNG.selected) + end + + fs[#fs + 1] = fmt(FMT.item_image_button, + X, Y + (sfinv_only and 0.7 or 0), + btn_size, btn_size, item, item, ESC(label)) + + local infos = { + unknown = not reg_items[name] or nil, + groups = groups, + burntime = fuel_cache[name], + cooktime = cooktime, + replace = replace, + } + + if next(infos) then + fs[#fs + 1] = get_tooltip(item, infos) + end + end + + if large_recipe then + fs[#fs + 1] = "style_type[item_image_button;border=false]" + end + + get_output_fs(fs, { + recipe = rcp, + shapeless = shapeless, + rightest = rightest, + btn_size = btn_size, + _btn_size = _btn_size, + spacing = spacing, + rarity = rarity, + }) +end + +local function get_panels(data, fs) + local panels = {recipes = data.recipes or {}, usages = data.usages or {}} + local x = 0.33 + + if sfinv_only then + panels = data.show_usages and {usages = data.usages} or {recipes = data.recipes} + end + + for k, v in pairs(panels) do + x = x + 1 + local spacing = (x - 1) * 3.6 + + if not sfinv_only then + fs[#fs + 1] = CORE_VERSION >= 510 and + fmt("background9[8.1,%f;6.6,3.5;%s;false;%d]", + -0.2 + spacing, PNG.bg_full, 10) or + fmt("background[8.1,%f;6.6,3.5;%s;false]", + -0.2 + spacing, PNG.bg_full) + end + + local rn = #v + local _rn = tostring(rn) + local xu = tostring(data.unum) .. _rn + local xr = tostring(data.rnum) .. _rn + xu = max(-0.3, -((#xu - 3) * 0.15)) + xr = max(-0.3, -((#xr - 3) * 0.15)) + + local is_recipe = k == "recipes" + local lbl + + if not sfinv_only and rn == 0 then + lbl = clr("#f00", is_recipe and ES"No recipes" or ES"No usages") + + elseif (not sfinv_only and is_recipe) or + (sfinv_only and not data.show_usages) then + lbl = ES("Recipe @1 of @2", data.rnum, rn) + + elseif not sfinv_only or (sfinv_only and data.show_usages) then + lbl = ES("Usage @1 of @2", data.unum, rn) + + elseif sfinv_only then + lbl = data.show_usages and + ES("Usage @1 of @2", data.unum, rn) or + ES("Recipe @1 of @2", data.rnum, rn) + end + + fs[#fs + 1] = fmt(FMT.label, + XOFFSET + (sfinv_only and 2.3 or 1.6) + (is_recipe and xr or xu), + YOFFSET + (sfinv_only and 3.35 or 1.5 + spacing), lbl) + + if rn > 1 then + local btn_suffix = is_recipe and "recipe" or "usage" + local prev_name = fmt("prev_%s", btn_suffix) + local next_name = fmt("next_%s", btn_suffix) + local x_arrow = XOFFSET + (sfinv_only and 1.7 or 1) + local y_arrow = YOFFSET + (sfinv_only and 3.25 or 1.4 + spacing) + + if CORE_VERSION >= 520 then + fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2), + x_arrow + (is_recipe and xr or xu), y_arrow, + PNG.prev, prev_name, "", + x_arrow + 1.8, y_arrow, PNG.next, next_name, "") + else + fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2), + x_arrow + (is_recipe and xr or xu), + y_arrow, PNG.prev, prev_name, PNG.prev_hover, + x_arrow + 1.8, y_arrow, PNG.next, next_name, PNG.next_hover) + end + end + + local rcp = is_recipe and v[data.rnum] or v[data.unum] + if rcp then + get_grid_fs(fs, rcp, spacing) + end + end +end + +local function make_formspec(name) + local data = pdata[name] + local fs = {} + + if not sfinv_only then + fs[#fs + 1] = fmt([[ + size[%f,%f] + no_prepend[] + bgcolor[#0000] + style_type[image_button;border=false] + style_type[item_image_button;border=false;bgimg_hovered=%s] + ]], + ROWS + (data.query_item and 6.7 or 0) - 1.2, LINES - 0.3, PNG.selected) + + fs[#fs + 1] = CORE_VERSION >= 510 and + fmt("background9[-0.15,-0.2;%f,%f;%s;false;%d]", + ROWS - 0.9, LINES + 0.4, PNG.bg_full, 10) or + fmt("background[-0.15,-0.2;%f,%f;%s;false]", + ROWS - 0.9, LINES + 0.4, PNG.bg_full) + end + + fs[#fs + 1] = fmt([[ + field[0.25,0.2;%f,1;filter;;%s] + field_close_on_enter[filter;false] + ]], + sfinv_only and 2.76 or 2.72, ESC(data.filter)) + + if CORE_VERSION >= 520 then + fs[#fs + 1] = fmt([[ + style[search;fgimg=%s;fgimg_hovered=%s] + style[clear;fgimg=%s;fgimg_hovered=%s] + style[prev_page;fgimg=%s;fgimg_hovered=%s] + style[next_page;fgimg=%s;fgimg_hovered=%s] + ]], + PNG.search, PNG.search_hover, + PNG.clear, PNG.clear_hover, + PNG.prev, PNG.prev_hover, + PNG.next, PNG.next_hover) + + fs[#fs + 1] = fmt(mul_elem(FMT.image_button, 4), + sfinv_only and 2.6 or 2.54, -0.06, 0.85, 0.85, "", "search", "", + sfinv_only and 3.3 or 3.25, -0.06, 0.85, 0.85, "", "clear", "", + sfinv_only and 5.45 or (ROWS * 6.83) / 11, -0.06, 0.85, 0.85, "", "prev_page", "", + sfinv_only and 7.2 or (ROWS * 8.75) / 11, -0.06, 0.85, 0.85, "", "next_page", "") + else + fs[#fs + 1] = fmt([[ + image_button[%f,-0.06;0.85,0.85;%s;search;;;false;%s] + image_button[%f,-0.06;0.85,0.85;%s;clear;;;false;%s] + ]], + sfinv_only and 2.6 or 2.54, PNG.search, PNG.search_hover, + sfinv_only and 3.3 or 3.25, PNG.clear, PNG.clear_hover) + + fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2), + sfinv_only and 5.45 or (ROWS * 6.83) / 11, -0.05, + PNG.prev, "prev_page", PNG.prev, + sfinv_only and 7.2 or (ROWS * 8.75) / 11, -0.05, + PNG.next, "next_page", PNG.next) + end + + data.pagemax = max(1, ceil(#data.items / IPP)) + + fs[#fs + 1] = fmt("label[%f,%f;%s / %u]", + sfinv_only and 6.35 or (ROWS * 7.85) / 11, + 0.06, clr("#ff0", data.pagenum), data.pagemax) + + if #data.items == 0 then + local no_item = S"No item to show" + local pos = ROWS / 3 + + if next(recipe_filters) and #init_items > 0 and data.filter == "" then + no_item = S"Collect items to reveal more recipes" + pos = pos - 1 + end + + fs[#fs + 1] = fmt(FMT.label, pos, 2, ESC(no_item)) + end + + local first_item = (data.pagenum - 1) * IPP + + for i = first_item, first_item + IPP - 1 do + local item = data.items[i + 1] + if not item then break end + + local X = i % ROWS + local Y = (i % IPP - X) / ROWS + 1 + + if CORE_VERSION >= 510 and data.query_item == item then + fs[#fs + 1] = fmt(FMT.image, + X - (X * (sfinv_only and 0.12 or 0.14)) - 0.05, + Y - (Y * 0.1) - 0.1, + 1, 1, PNG.selected) + end + + fs[#fs + 1] = fmt("item_image_button[%f,%f;%f,%f;%s;%s_inv;]", + X - (X * (sfinv_only and 0.12 or 0.14)) - 0.05, + Y - (Y * 0.1) - 0.1, + 1, 1, item, item) + end + + if (data.recipes and #data.recipes > 0) or (data.usages and #data.usages > 0) then + get_panels(data, fs) + end + + return concat(fs) +end + +local show_fs = function(player, name) + if sfinv_only then + sfinv.set_player_inventory_formspec(player) + else + show_formspec(name, "craftguide", make_formspec(name)) + end +end + +craftguide.register_craft_type("digging", { + description = ES"Digging", + icon = "default_tool_steelpick.png", +}) + +craftguide.register_craft_type("digging_chance", { + description = ES"Digging Chance", + icon = "default_tool_mesepick.png", +}) + +local function sfind(str, filter) + if filter == "" then + return 0 + end + + if find(str, filter, 1, true) then + return #str - #filter + end +end + +local function search(data) + local filter = data.filter + + if searches[filter] then + data.items = searches[filter] + return + end + + local opt = "^(.-)%+([%w_]+)=([%w_,]+)" + local search_filter = next(search_filters) and match(filter, opt) + local filters = {} + + if search_filter then + for filter_name, values in gmatch(filter, sub(opt, 6)) do + if search_filters[filter_name] then + values = split(values, ",") + filters[filter_name] = values + end + end + end + + local filtered_list, order, c = {}, {}, 0 + + for i = 1, #data.items_raw do + local item = data.items_raw[i] + local def = reg_items[item] + local desc = (def and def.description) and lower(def.description) or "" + local to_add + + if search_filter then + for filter_name, values in pairs(filters) do + local func = search_filters[filter_name] + to_add = func(item, values) and (search_filter == "" or + (sfind(item, search_filter) or sfind(desc, search_filter))) + end + else + to_add = sfind(item, filter) or sfind(desc, filter) + end + + if to_add then + c = c + 1 + filtered_list[c] = item + order[item] = to_add + end + end + + if not next(recipe_filters) then + -- Cache the results only if searched 2 times + if searches[filter] == nil then + searches[filter] = false + else + searches[filter] = filtered_list + end + end + + sort(filtered_list, function(a, b) + return order[a] < order[b] + end) + + data.items = filtered_list +end + +craftguide.add_search_filter("groups", function(item, groups) + local def = reg_items[item] + local has_groups = true + + for i = 1, #groups do + local group = groups[i] + if not def.groups[group] then + has_groups = nil + break + end + end + + return has_groups +end) + +--[[ As `core.get_craft_recipe` and `core.get_all_craft_recipes` do not + return the replacements and toolrepair, we have to override + `core.register_craft` and do some reverse engineering. + See engine's issues #4901 and #8920. ]] + +fuel_cache.replacements = {} + +local old_register_craft = core.register_craft + +core.register_craft = function(def) + old_register_craft(def) + + if def.type == "toolrepair" then + toolrepair = def.additional_wear * -100 + end + + local output = def.output or (true_str(def.recipe) and def.recipe) or nil + if not output then return end + output = {match(output, "%S+")} + + local groups + + if is_group(output[1]) then + groups = extract_groups(output[1]) + output = groups_to_items(groups, true) + end + + for i = 1, #output do + local name = output[i] + + if def.type ~= "fuel" then + def.items = {} + end + + if def.type == "fuel" then + fuel_cache[name] = def.burntime + fuel_cache.replacements[name] = def.replacements + + elseif def.type == "cooking" then + def.width = def.cooktime + def.cooktime = nil + def.items[1] = def.recipe + + elseif def.type == "shapeless" then + def.width = 0 + for j = 1, #def.recipe do + def.items[#def.items + 1] = def.recipe[j] + end + else + def.width = #def.recipe[1] + local c = 0 + + for j = 1, #def.recipe do + if def.recipe[j] then + for h = 1, def.width do + c = c + 1 + local it = def.recipe[j][h] + + if it and it ~= "" then + def.items[c] = it + end + end + end + end + end + + if def.type ~= "fuel" then + def.recipe = nil + recipes_cache[name] = recipes_cache[name] or {} + insert(recipes_cache[name], 1, def) + end + end +end + +local old_clear_craft = core.clear_craft + +core.clear_craft = function(def) + old_clear_craft(def) + + if true_str(def) then + recipes_cache[def] = nil + fuel_cache[def] = nil + + elseif is_table(def) then + return -- TODO + end +end + +local function handle_drops_table(name, drop) + -- Code borrowed and modified from unified_inventory + -- https://github.com/minetest-mods/unified_inventory/blob/master/api.lua + local drop_sure, drop_maybe = {}, {} + local drop_items = drop.items or {} + local max_items_left = drop.max_items + local max_start = true + + for i = 1, #drop_items do + if max_items_left and max_items_left <= 0 then break end + local di = drop_items[i] + + for j = 1, #di.items do + local dstack = ItemStack(di.items[j]) + local dname = dstack:get_name() + + if not dstack:is_empty() and dname ~= name then + local dcount = dstack:get_count() + + if #di.items == 1 and di.rarity == 1 and max_start then + if not drop_sure[dname] then + drop_sure[dname] = 0 + end + + drop_sure[dname] = drop_sure[dname] + dcount + + if max_items_left then + max_items_left = max_items_left - 1 + if max_items_left <= 0 then break end + end + else + if max_items_left then + max_start = false + end + + if not drop_maybe[dname] then + drop_maybe[dname] = {} + end + + if not drop_maybe[dname].output then + drop_maybe[dname].output = 0 + end + + drop_maybe[dname] = { + output = drop_maybe[dname].output + dcount, + rarity = di.rarity, + } + end + end + end + end + + for item, count in pairs(drop_sure) do + craftguide.register_craft({ + type = "digging", + items = {name}, + output = fmt("%s %u", item, count), + }) + end + + for item, data in pairs(drop_maybe) do + craftguide.register_craft({ + type = "digging_chance", + items = {name}, + output = fmt("%s %u", item, data.output), + rarity = data.rarity, + }) + end +end + +local function register_drops(name, def) + local drop = def.drop + local dstack = ItemStack(drop) + + if not dstack:is_empty() and dstack:get_name() ~= name then + craftguide.register_craft({ + type = "digging", + items = {name}, + output = drop, + }) + elseif is_table(drop) then + handle_drops_table(name, drop) + end +end + +local function handle_aliases(hash) + for oldname, newname in pairs(reg_aliases) do + cache_recipes(oldname) + local recipes = recipes_cache[oldname] + + if recipes then + if not recipes_cache[newname] then + recipes_cache[newname] = {} + end + + local similar + + for i = 1, #recipes_cache[oldname] do + local rcp_old = recipes_cache[oldname][i] + + for j = 1, #recipes_cache[newname] do + local rcp_new = recipes_cache[newname][j] + rcp_new.type = nil + rcp_new.method = nil + + if table_eq(rcp_old, rcp_new) then + similar = true + break + end + end + + if not similar then + insert(recipes_cache[newname], rcp_old) + end + end + end + + if newname ~= "" and recipes_cache[oldname] and not hash[newname] then + init_items[#init_items + 1] = newname + end + end +end + +local function show_item(def) + return not (def.groups.not_in_craft_guide == 1 or + def.groups.not_in_creative_inventory == 1) and + def.description and def.description ~= "" +end + +local function tablelen(t) + local c = 0 + for _ in pairs(t) do + c = c + 1 + end + + return c +end + +local function get_init_items() + local ic, it, last_str = 0, tablelen(reg_items), "" + local hash = {} + + local function iop(str) + io.write(("\b \b"):rep(#last_str)) + io.write(str) + io.flush() + last_str = str + end + + local full_char, empty_char = "▰", "▱" + + for name, def in pairs(reg_items) do + ic = ic + 1 + local percent, bar, len = (ic * 100) / it, "", 20 + + for i = 1, len do + bar = bar .. (i <= percent / (100 / len) and full_char or empty_char) + end + + iop(fmt("[craftguide] Caching data %s %u%%\r", bar, percent)) + + if show_item(def) then + if not fuel_cache[name] then + cache_fuel(name) + end + + if not recipes_cache[name] then + cache_recipes(name) + end + + cache_usages(name) + register_drops(name, def) + + if name ~= "" and recipes_cache[name] or usages_cache[name] then + init_items[#init_items + 1] = name + hash[name] = true + end + end + end + + handle_aliases(hash) + sort(init_items) + + if http and true_str(craftguide.export_url) then + local post_data = { + recipes = recipes_cache, + usages = usages_cache, + fuel = fuel_cache, + } + + http.fetch_async({ + url = craftguide.export_url, + post_data = write_json(post_data), + }) + end + + print() +end + +local function init_data(name) + local items = CORE_VERSION >= 500 and init_items or + (#init_items == 0 and get_init_items() or init_items) + + pdata[name] = { + filter = "", + pagenum = 1, + items = items, + items_raw = items, + } +end + +local function reset_data(data) + data.filter = "" + data.pagenum = 1 + data.rnum = 1 + data.unum = 1 + data.query_item = nil + data.recipes = nil + data.usages = nil + data.show_usages = nil + data.items = data.items_raw +end + +if CORE_VERSION >= 500 then + on_mods_loaded(get_init_items) +end + +on_joinplayer(function(player) + local name = player:get_player_name() + init_data(name) +end) + +local function fields(player, _f) + local name = player:get_player_name() + local data = pdata[name] + + if _f.clear then + reset_data(data) + return true, show_fs(player, name) + + elseif _f.prev_recipe or _f.next_recipe then + local num = data.rnum + (_f.prev_recipe and -1 or 1) + data.rnum = data.recipes[num] and num or (_f.prev_recipe and #data.recipes or 1) + return true, show_fs(player, name) + + elseif _f.prev_usage or _f.next_usage then + local num = data.unum + (_f.prev_usage and -1 or 1) + data.unum = data.usages[num] and num or (_f.prev_usage and #data.usages or 1) + return true, show_fs(player, name) + + elseif (_f.key_enter_field == "filter" or _f.search) and _f.filter ~= "" then + local str = lower(_f.filter) + if data.filter == str then return end + + data.filter = str data.pagenum = 1 - get_filter_items(data, player) - show_fs(player, player_name) + search(data) - elseif fields.prev or fields.next then - data.pagenum = data.pagenum - (fields.prev and 1 or -1) + return true, show_fs(player, name) + + elseif _f.prev_page or _f.next_page then + if data.pagemax == 1 then return end + data.pagenum = data.pagenum - (_f.prev_page and 1 or -1) if data.pagenum > data.pagemax then data.pagenum = 1 @@ -598,149 +1536,146 @@ local function get_fields(player, ...) data.pagenum = data.pagemax end - show_fs(player, player_name) - - elseif (fields.size_inc and data.iX < MAX_LIMIT) or - (fields.size_dec and data.iX > MIN_LIMIT) then - data.pagenum = 1 - data.iX = data.iX - (fields.size_dec and 1 or -1) - show_fs(player, player_name) - - else for item in pairs(fields) do - if item:find(":") then - if item:sub(-4) == "_inv" then - item = item:sub(1,-5) - elseif item:find("%s") then - item = item:match("%S*") - end - - local is_fuel = get_fueltime(item) > 0 - local recipes = get_recipes(item) or {} - recipes = add_custom_recipes(item, recipes) - if not next(recipes) and not is_fuel then return end - - if not data.show_usage and item == data.item and not progressive_mode then - data.usages = get_item_usages(item) - if next(data.usages) then - data.show_usage = true - data.rnum = 1 - end - - show_fs(player, player_name) - else - if progressive_mode then - local inv = player:get_inventory() - local has_item - recipes, has_item = recipe_in_inv(inv, item, recipes) - if not has_item then return end - end - - data.item = item - data.recipes_item = recipes - data.rnum = 1 - data.show_usage = nil - data.fuel = is_fuel - - show_fs(player, player_name) + return true, show_fs(player, name) + else + local item + for field in pairs(_f) do + if find(field, ":") then + item = field + break end end - end + + if not item then + return + elseif sub(item, -4) == "_inv" then + item = sub(item, 1, -5) + end + + item = reg_aliases[item] or item + + if sfinv_only then + if item ~= data.query_item then + data.show_usages = nil + else + data.show_usages = not data.show_usages + end + end + + local recipes, usages = get_recipes(item, data, player) + if not recipes and not usages then return end + + data.query_item = item + data.recipes = recipes + data.usages = usages + data.rnum = 1 + data.unum = 1 + + return true, show_fs(player, name) end end if sfinv_only then sfinv.register_page("craftguide:craftguide", { - title = "Craft Guide", - get = function(self, player, context) - local player_name = player:get_player_name() - return sfinv.make_formspec( - player, - context, - get_formspec(player_name) - ) - end, - on_enter = function(self, player, context) - local player_name = player:get_player_name() - local data = datas[player_name] + title = S"Craft Guide", - if progressive_mode or not data then - init_datas(player, player_name) + get = function(self, player, context) + local name = player:get_player_name() + local formspec = make_formspec(name) + + return sfinv.make_formspec(player, context, formspec) + end, + + on_enter = function(self, player, context) + if next(recipe_filters) then + local name = player:get_player_name() + local data = pdata[name] + + data.items_raw = get_filtered_items(player) + search(data) end end, - on_player_receive_fields = function(self, player, context, fields) - get_fields(player, fields) + + on_player_receive_fields = function(self, player, context, _f) + fields(player, _f) end, }) else - mt.register_on_player_receive_fields(get_fields) - - local function on_use(itemstack, user) - local player_name = user:get_player_name() - local data = datas[player_name] - - if progressive_mode or not data then - init_datas(user, player_name) - get_formspec(player_name) - else - show_formspec(player_name, "craftguide", data.formspec) + on_receive_fields(function(player, formname, _f) + if formname == "craftguide" then + fields(player, _f) end + end) + + local function on_use(user) + local name = user:get_player_name() + + if next(recipe_filters) then + local data = pdata[name] + data.items_raw = get_filtered_items(user) + search(data) + end + + show_formspec(name, "craftguide", make_formspec(name)) end - mt.register_craftitem("craftguide:book", { - description = S("Crafting Guide"), - inventory_image = "craftguide_book.png", - wield_image = "craftguide_book.png", + core.register_craftitem("craftguide:book", { + description = S"Crafting Guide", + inventory_image = PNG.book, + wield_image = PNG.book, stack_max = 1, groups = {book = 1}, on_use = function(itemstack, user) - on_use(itemstack, user) + on_use(user) end }) - mt.register_node("craftguide:sign", { - description = S("Crafting Guide Sign"), + core.register_node("craftguide:sign", { + description = S"Crafting Guide Sign", drawtype = "nodebox", - tiles = {"craftguide_sign.png"}, - inventory_image = "craftguide_sign_inv.png", - wield_image = "craftguide_sign_inv.png", + tiles = {PNG.sign}, + inventory_image = PNG.sign, + wield_image = PNG.sign, paramtype = "light", paramtype2 = "wallmounted", sunlight_propagates = true, - groups = {wood = 1, oddly_breakable_by_hand = 1, flammable = 3}, + groups = {oddly_breakable_by_hand = 1, flammable = 3}, node_box = { type = "wallmounted", - wall_top = {-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}, - wall_bottom = {-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}, - wall_side = {-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375} + wall_top = {-0.5, 0.4375, -0.5, 0.5, 0.5, 0.5}, + wall_bottom = {-0.5, -0.5, -0.5, 0.5, -0.4375, 0.5}, + wall_side = {-0.5, -0.5, -0.5, -0.4375, 0.5, 0.5} }, + on_construct = function(pos) - local meta = mt.get_meta(pos) - meta:set_string("infotext", S("Crafting Guide Sign")) + local meta = core.get_meta(pos) + meta:set_string("infotext", "Crafting Guide Sign") end, + on_rightclick = function(pos, node, user, itemstack) - on_use(itemstack, user) + on_use(user) end }) - mt.register_craft({ + core.register_craft({ output = "craftguide:book", - type = "shapeless", + type = "shapeless", recipe = {"default:book"} }) - mt.register_craft({ + core.register_craft({ type = "fuel", recipe = "craftguide:book", burntime = 3 }) - mt.register_craft({ + core.register_craft({ output = "craftguide:sign", - type = "shapeless", + type = "shapeless", recipe = {"default:sign_wall_wood"} }) - mt.register_craft({ + core.register_craft({ type = "fuel", recipe = "craftguide:sign", burntime = 10 @@ -748,88 +1683,350 @@ else if rawget(_G, "sfinv_buttons") then sfinv_buttons.register_button("craftguide", { - title = S("Crafting Guide"), - tooltip = S("Shows a list of available crafting recipes, cooking recipes and fuels"), + title = S"Crafting Guide", + tooltip = S"Shows a list of available crafting recipes, cooking recipes and fuels", + image = PNG.book, action = function(player) - on_use(nil, player) + on_use(player) end, - image = "craftguide_book.png", }) end end -if not progressive_mode then - mt.register_chatcommand("craft", { - description = S("Show recipe(s) of the pointed node"), - func = function(name) - local player = mt.get_player_by_name(name) - local ppos = player:get_pos() - local dir = player:get_look_dir() - local eye_h = {x = ppos.x, y = ppos.y + 1.625, z = ppos.z} - local node_name +if progressive_mode then + local PLAYERS = {} + local POLL_FREQ = 0.25 + local HUD_TIMER_MAX = 1.5 - for i = 1, 10 do - local look_at = vector_add(eye_h, vector_mul(dir, i)) - local node = mt.get_node(look_at) + local function item_in_inv(item, inv_items) + local inv_items_size = #inv_items - if node.name ~= "air" then - node_name = node.name - break + if is_group(item) then + local groups = extract_groups(item) + for i = 1, inv_items_size do + local def = reg_items[inv_items[i]] + + if def then + local item_groups = def.groups + if item_has_groups(item_groups, groups) then + return true + end end end - - if not node_name then - return false, colorize("[craftguide] ") .. S("No node pointed") - elseif not datas[name] then - init_datas(player, name) + else + for i = 1, inv_items_size do + if inv_items[i] == item then + return true + end end - - local data = datas[name] - local is_fuel = get_fueltime(node_name) > 0 - local recipes = get_recipes(node_name) or {} - recipes = add_custom_recipes(node_name, recipes) - - if not next(recipes) and not is_fuel then - return false, colorize("[craftguide] ") .. - S("No recipe for this node:") .. " " .. - colorize(node_name) - end - - data.show_usage = nil - data.filter = "" - data.item = node_name - data.pagenum = 1 - data.rnum = 1 - data.recipes_item = recipes - data.items = datas.init_items - data.fuel = is_fuel - - return true, show_fs(player, name) - end, - }) -end - ---[[ Custom recipes (>3x3) test code - -mt.register_craftitem("craftguide:custom_recipe_test", { - description = "Custom Recipe Test", -}) - -local cr = {} -for x = 1, 6 do - cr[x] = {} - for i = 1, 10 - x do - cr[x][i] = {} - for j = 1, 10 - x do - cr[x][i][j] = "group:wood" end end - mt.register_craft({ - output = "craftguide:custom_recipe_test", - recipe = cr[x] - }) + local function recipe_in_inv(recipe, inv_items) + for _, item in pairs(recipe.items) do + if not item_in_inv(item, inv_items) then return end + end + + return true + end + + local function progressive_filter(recipes, player) + if not recipes then + return {} + end + + local name = player:get_player_name() + local data = pdata[name] + + if #data.inv_items == 0 then + return {} + end + + local filtered, c = {}, 0 + for i = 1, #recipes do + local recipe = recipes[i] + if recipe_in_inv(recipe, data.inv_items) then + c = c + 1 + filtered[c] = recipe + end + end + + return filtered + end + + local item_lists = { + "main", + "craft", + "craftpreview", + } + + local function get_inv_items(player) + local inv = player:get_inventory() + local stacks = {} + + for i = 1, #item_lists do + local list = inv:get_list(item_lists[i]) + table_merge(stacks, list) + end + + local inv_items, c = {}, 0 + + for i = 1, #stacks do + local stack = stacks[i] + if not stack:is_empty() then + local name = stack:get_name() + if reg_items[name] then + c = c + 1 + inv_items[c] = name + end + end + end + + return inv_items + end + + local function show_hud_success(player, data) + -- It'd better to have an engine function `hud_move` to only need + -- 2 calls for the notification's back and forth. + + local hud_info_bg = player:hud_get(data.hud.bg) + local dt = 0.016 + + if hud_info_bg.position.y <= 0.9 then + data.show_hud = false + data.hud_timer = (data.hud_timer or 0) + dt + end + + if data.show_hud then + for _, def in pairs(data.hud) do + local hud_info = player:hud_get(def) + + player:hud_change(def, "position", { + x = hud_info.position.x, + y = hud_info.position.y - (dt / 5) + }) + end + + player:hud_change(data.hud.text, "text", + S("@1 new recipe(s) discovered!", data.discovered)) + + elseif data.show_hud == false then + if data.hud_timer >= HUD_TIMER_MAX then + for _, def in pairs(data.hud) do + local hud_info = player:hud_get(def) + + player:hud_change(def, "position", { + x = hud_info.position.x, + y = hud_info.position.y + (dt / 5) + }) + end + + if hud_info_bg.position.y >= 1 then + data.show_hud = nil + data.hud_timer = nil + end + end + end + end + + -- Workaround. Need an engine call to detect when the contents of + -- the player inventory changed, instead. + local function poll_new_items() + for i = 1, #PLAYERS do + local player = PLAYERS[i] + local name = player:get_player_name() + local data = pdata[name] + + local inv_items = get_inv_items(player) + local diff = array_diff(inv_items, data.inv_items) + + if #diff > 0 then + data.inv_items = table_merge(diff, data.inv_items) + + local oldknown = data.known_recipes or 0 + local items = get_filtered_items(player, data) + + data.discovered = data.known_recipes - oldknown + + if data.show_hud == nil and data.discovered > 0 then + data.show_hud = true + end + + if sfinv_only then + data.items_raw = items + search(data) + sfinv.set_player_inventory_formspec(player) + end + end + end + + after(POLL_FREQ, poll_new_items) + end + + poll_new_items() + + globalstep(function() + for i = 1, #PLAYERS do + local player = PLAYERS[i] + local name = player:get_player_name() + local data = pdata[name] + + if data.show_hud ~= nil then + show_hud_success(player, data) + end + end + end) + + craftguide.add_recipe_filter("Default progressive filter", progressive_filter) + + on_joinplayer(function(player) + PLAYERS = get_players() + + local name = player:get_player_name() + local data = pdata[name] + + if CORE_VERSION >= 500 then + local meta = player:get_meta() + data.inv_items = dslz(meta:get_string("inv_items")) or {} + data.known_recipes = dslz(meta:get_string("known_recipes")) or 0 + else + data.inv_items = dslz(player:get_attribute("inv_items")) or {} + data.known_recipes = dslz(player:get_attribute("known_recipes")) or 0 + end + + data.hud = { + bg = player:hud_add({ + hud_elem_type = "image", + position = {x = 0.78, y = 1}, + alignment = {x = 1, y = 1}, + scale = {x = 370, y = 112}, + text = PNG.bg, + }), + + book = player:hud_add({ + hud_elem_type = "image", + position = {x = 0.79, y = 1.02}, + alignment = {x = 1, y = 1}, + scale = {x = 4, y = 4}, + text = PNG.book, + }), + + text = player:hud_add({ + hud_elem_type = "text", + position = {x = 0.84, y = 1.04}, + alignment = {x = 1, y = 1}, + number = 0xfff, + text = "", + }), + } + end) + + local to_save = { + "inv_items", + "known_recipes", + } + + local function save_meta(player) + local meta + local name = player:get_player_name() + local data = pdata[name] + + if CORE_VERSION >= 500 then + meta = player:get_meta() + end + + for i = 1, #to_save do + local meta_name = to_save[i] + + if CORE_VERSION >= 500 then + meta:set_string(meta_name, slz(data[meta_name])) + else + player:set_attribute(meta_name, slz(data[meta_name])) + end + end + end + + on_leaveplayer(function(player) + PLAYERS = get_players() + save_meta(player) + end) + + on_shutdown(function() + for i = 1, #PLAYERS do + local player = PLAYERS[i] + save_meta(player) + end + end) end -]] + +on_leaveplayer(function(player) + local name = player:get_player_name() + pdata[name] = nil +end) + +function craftguide.show(name, item, show_usages) + if not true_str(name)then + return err"craftguide.show(): player name missing" + end + + local data = pdata[name] + local player = get_player_by_name(name) + local query_item = data.query_item + + reset_data(data) + + item = reg_items[item] and item or query_item + local recipes, usages = get_recipes(item, data, player) + + if not recipes and not usages then + if not recipes_cache[item] and not usages_cache[item] then + return false, msg(name, fmt("%s: %s", + S"No recipe or usage for this item", + get_desc(item))) + end + + return false, msg(name, fmt("%s: %s", + S"You don't know a recipe or usage for this item", + get_desc(item))) + end + + data.query_item = item + data.recipes = recipes + data.usages = usages + + if sfinv_only then + data.show_usages = show_usages + end + + show_fs(player, name) +end + +register_command("craft", { + description = S"Show recipe(s) of the pointed node", + func = function(name) + local player = get_player_by_name(name) + local dir = player:get_look_dir() + local ppos = player:get_pos() + ppos.y = ppos.y + 1.625 + + local node_name + + for i = 1, 10 do + local look_at = vec_add(ppos, vec_mul(dir, i)) + local node = core.get_node(look_at) + + if node.name ~= "air" then + node_name = node.name + break + end + end + + if not node_name then + return false, msg(name, S"No node pointed") + end + + return true, craftguide.show(name, node_name) + end, +}) minetest.log("action", "[craftguide] loaded.") diff --git a/intllib.lua b/intllib.lua deleted file mode 100644 index 6669d72..0000000 --- a/intllib.lua +++ /dev/null @@ -1,45 +0,0 @@ - --- Fallback functions for when `intllib` is not installed. --- Code released under Unlicense . - --- Get the latest version of this file at: --- https://raw.githubusercontent.com/minetest-mods/intllib/master/lib/intllib.lua - -local function format(str, ...) - local args = { ... } - local function repl(escape, open, num, close) - if escape == "" then - local replacement = tostring(args[tonumber(num)]) - if open == "" then - replacement = replacement..close - end - return replacement - else - return "@"..open..num..close - end - end - return (str:gsub("(@?)@(%(?)(%d+)(%)?)", repl)) -end - -local gettext, ngettext -if minetest.get_modpath("intllib") then - if intllib.make_gettext_pair then - -- New method using gettext. - gettext, ngettext = intllib.make_gettext_pair() - else - -- Old method using text files. - gettext = intllib.Getter() - end -end - --- Fill in missing functions. - -gettext = gettext or function(msgid, ...) - return format(msgid, ...) -end - -ngettext = ngettext or function(msgid, msgid_plural, n, ...) - return format(n==1 and msgid or msgid_plural, ...) -end - -return gettext, ngettext diff --git a/LICENSE b/license.txt similarity index 56% rename from LICENSE rename to license.txt index 7854fd0..57174d4 100644 --- a/LICENSE +++ b/license.txt @@ -1,15 +1,28 @@ License of source code ---------------------- -« Copyright © 2015-2018, Jean-Patrick Guerrero +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2015-2019 Jean-Patrick Guerrero and contributors. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders X be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. -Except as contained in this notice, the name of the shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the . » Licenses of media (textures) ---------------------------- diff --git a/locale/craftguide.de.tr b/locale/craftguide.de.tr new file mode 100644 index 0000000..807012e --- /dev/null +++ b/locale/craftguide.de.tr @@ -0,0 +1,19 @@ +# textdomain: craftguide + +Craft Guide=Rezeptbuch +Crafting Guide=Rezeptbuch +Crafting Guide Sign=Rezepttafel +Usage @1 of @2=Verwendung @1 von @2 +Recipe @1 of @2=Rezept @1 von @2 +Burning time: @1=Brennzeit: @1 +Cooking time: @1=Kochzeit: @1 +Any item belonging to the group(s): @1=Beliebiger Gegenstand aus Gruppe(n): @1 +Recipe's too big to be displayed (@1x@2)=Rezept ist zu groß für die Anzeige (@1×@2) +Shapeless=Formlos +Cooking=Kochen +No item to show=Nichts anzuzeigen +Collect items to reveal more recipes=Gegenstände aufsammeln, um mehr Rezepte aufzudecken +Show recipe(s) of the pointed node=Rezept(e) des gezeigten Blocks anzeigen +No node pointed=Auf keinen Block gezeigt +You don't know a recipe for this node=Sie kennen kein Rezept für diesen Block +No recipe for this node=Kein Rezept für diesen Block diff --git a/locale/craftguide.fr.tr b/locale/craftguide.fr.tr new file mode 100644 index 0000000..f624eaa --- /dev/null +++ b/locale/craftguide.fr.tr @@ -0,0 +1,22 @@ +# textdomain: craftguide + +Craft Guide=Guide de recettes +Crafting Guide=Guide de recettes +Usage @1 of @2=Usage @1 de @2 +Recipe @1 of @2=Recette @1 de @2 +Burning time: @1=Temps de combustion : @1 +Cooking time: @1=Temps de cuisson : @1 +Replaced by @1 on smelting=Remplacé par @1 lors de la cuisson +Replaced by @1 on burning=Remplacé par @1 lors de la combustion +Replaced by @1 on crafting=Remplacé par @1 lors de la fabrication +Repairable by step of @1=Réparable par étape de @1 +Any item belonging to the group(s): @1=Tout item appartenant au(x) groupe(s) : @1 +Recipe's too big to be displayed (@1x@2)=La recette est trop grande pour être affichée (@1x@2) +Shapeless=Sans forme +Cooking=Cuisson +No item to show=Aucun item à afficher +Collect items to reveal more recipes=Collecte des items pour révéler plus de recettes +Show recipe(s) of the pointed node=Affiche les recettes du bloc visé +No node pointed=Aucun bloc visé +You don't know a recipe for this node=Tu ne connais aucune recette pour ce bloc +No recipe for this node=Aucune recette pour ce bloc diff --git a/locale/craftguide.it.tr b/locale/craftguide.it.tr new file mode 100644 index 0000000..4a0c9ae --- /dev/null +++ b/locale/craftguide.it.tr @@ -0,0 +1,26 @@ +# textdomain: craftguide + +Craft Guide=Guida di assemblaggio +Crafting Guide=Guida d'assemblaggio +Crafting Guide Sign=Cartello della guida d'assemblaggio +Usage @1 of @2=Utilizzo @1 di @2 +Recipe @1 of @2=Ricetta @1 di @2 +Burning time: @1=Tempo di bruciatura: @1 +Cooking time: @1=Tempo di cottura: @1 +Replaced by @1 on smelting=Sostituito da @1 alla fusione +Replaced by @1 on burning=Sostituito da @1 alla bruciatura +Replaced by @1 on crafting=Sostituito da @1 all'assemblaggio +Repairable by step of @1=Riparabile per passo di @1 +Any item belonging to the group(s): @1=Qualunque oggetto appartenente al gruppo: @1 +Recipe's too big to be displayed (@1x@2)=La ricetta è troppo grande per essere mostrata (@1x@2) +Shapeless=Senza forma +Cooking=Cottura +No item to show=Nessun oggetto da mostrare +Collect items to reveal more recipes=Raccogli oggetti per svelare più ricette +Show recipe(s) of the pointed node=Mostra la ricetta del nodo puntato +No node pointed=Nessun nodo puntato +You don't know a recipe for this node=Non conosci una ricetta per questo nodo +No recipe for this node=Nessuna ricetta per questo nodo +Digging=Scavando +Digging Chance=Probabilità di scavare +@1 of chance to drop=@1 di probabilità di rilascio diff --git a/locale/craftguide.ru.tr b/locale/craftguide.ru.tr new file mode 100644 index 0000000..fb023e1 --- /dev/null +++ b/locale/craftguide.ru.tr @@ -0,0 +1,19 @@ +# textdomain: craftguide + +Craft Guide=книга рецептов крафта +Crafting Guide=книга рецептов крафта +Crafting Guide Sign=Знак с книгой рецептов +Usage @1 of @2=использование @1 из @2 +Recipe @1 of @2=Рецепт @1 из @2 +Burning time: @1=Время горения: @1 +Cooking time: @1=Время преготовления: @1 +Any item belonging to the group(s): @1=Любой элемент из группы: @1 +Recipe's too big to be displayed (@1x@2)=Рецепт слишком большой для показа (@1x@2) +Shapeless=Бесформенный +Cooking=Приготовление +No item to show=Нет элемента для показа +Collect items to reveal more recipes=Собирайте предметы, чтобы раскрыть больше рецептов +Show recipe(s) of the pointed node=Показать рецепт(ы) выбранной ноды +No node pointed=Не указана нода +You don't know a recipe for this node=Вы не знаете рецепт для этой ноды +No recipe for this node=Нет рецептов для этой ноды diff --git a/locale/de.po b/locale/de.po deleted file mode 100644 index 8e5a8de..0000000 --- a/locale/de.po +++ /dev/null @@ -1,90 +0,0 @@ -# German translation for craftguide mod. -# Copyright (C) 2018 -# This file is distributed under the same license as the craftguide package. -# codexp , 2018. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 1.27\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-23 00:17+0100\n" -"PO-Revision-Date: \n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: German\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: init.lua -msgid "Unknown Item (@1)" -msgstr "Unbekannter Gegenstand (@1)" - -#: init.lua -msgid "Any item belonging to the group(s)" -msgstr "Beliebiger Gegenstand aus der Gruppe" - -#: init.lua -msgid "Cooking time" -msgstr "Kochzeit" - -#: init.lua -msgid "Burning time" -msgstr "Brennzeit" - -#: init.lua -msgid "Alternate" -msgstr "Andere" - -#: init.lua -msgid "Recipe @1 of @2" -msgstr "Rezept @1 von @2" - -#: init.lua -msgid "" -"Recipe is too big to\n" -"be displayed (@1x@2)" -msgstr "" -"Rezept ist zu groß\n" -"für die Anzeige (@1x@2)" - -#: init.lua -msgid "Search" -msgstr "Suche" - -#: init.lua -msgid "Reset" -msgstr "Zurücksetzen" - -#: init.lua -msgid "Increase window size" -msgstr "Fenster vergrößern" - -#: init.lua -msgid "Decrease window size" -msgstr "Fenster verkleinern" - -#: init.lua -msgid "Previous page" -msgstr "Vorherige Seite" - -#: init.lua -msgid "Next page" -msgstr "Nächste Seite" - -#: init.lua -msgid "No item to show" -msgstr "Nichts anzuzeigen" - -#: init.lua -msgid "Crafting Guide" -msgstr "Rezeptbuch" - -#: init.lua -msgid "Crafting Guide Sign" -msgstr "Rezepttafel" - -#: init.lua -msgid "Shows a list of available crafting recipes, cooking recipes and fuels" -msgstr "Zeigt eine Liste von verfügbaren Rezepten, Kochrezepten und Brennmaterialien" diff --git a/locale/ms.po b/locale/ms.po deleted file mode 100644 index 56a1c20..0000000 --- a/locale/ms.po +++ /dev/null @@ -1,91 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: craftguide\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-23 18:35+0100\n" -"PO-Revision-Date: 2018-04-18 02:02+0800\n" -"Language-Team: muhdnurhidayat \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.0.6\n" -"Last-Translator: MuhdNurHidayat (MNH48) \n" -"Plural-Forms: nplurals=1; plural=0;\n" -"Language: ms\n" - -#: init.lua -msgid "Unknown Item (@1)" -msgstr "Item Tidak Diketahui (@1)" - -#: init.lua -msgid "Any item belonging to the group(s)" -msgstr "Sebarang item dari kumpulan" - -#: init.lua -msgid "Cooking time" -msgstr "Tempoh memasak" - -#: init.lua -msgid "Burning time" -msgstr "Tempoh pembakaran" - -#: init.lua -msgid "Alternate" -msgstr "Resipi lain" - -#: init.lua -msgid "Recipe @1 of @2" -msgstr "Resipi @1 dari @2" - -#: init.lua -msgid "" -"Recipe is too big to\n" -"be displayed (@1x@2)" -msgstr "" -"Resipi terlalu besar\n" -"untuk paparan (@1x@2)" - -#: init.lua -msgid "Search" -msgstr "Cari" - -#: init.lua -msgid "Reset" -msgstr "Set semula" - -#: init.lua -msgid "Increase window size" -msgstr "Besarkan saiz tetingkap" - -#: init.lua -msgid "Decrease window size" -msgstr "Kecilkan saiz tetingkap" - -#: init.lua -msgid "Previous page" -msgstr "Halaman sebelumnya" - -#: init.lua -msgid "Next page" -msgstr "Halaman seterusnya" - -#: init.lua -msgid "No item to show" -msgstr "Tiada item untuk dipaparkan" - -#: init.lua -msgid "Crafting Guide" -msgstr "Panduan Pertukangan" - -#: init.lua -msgid "Crafting Guide Sign" -msgstr "Papan Tanda Panduan Pertukangan" - -#: init.lua -msgid "Shows a list of available crafting recipes, cooking recipes and fuels" -msgstr "Menunjukkan senarai resipi pertukangan, resipi memasak dan bahan api yang ada" diff --git a/locale/ru.po b/locale/ru.po deleted file mode 100644 index e99d0c6..0000000 --- a/locale/ru.po +++ /dev/null @@ -1,90 +0,0 @@ -# Russian translation for craftguide mod. -# Copyright (C) 2018 -# This file is distributed under the same license as the craftguide package. -# codexp , 2018. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 1.27\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-23 00:17+0100\n" -"PO-Revision-Date: \n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: Russian\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: init.lua -msgid "Unknown Item (@1)" -msgstr "Неизвестный элемент (@1)" - -#: init.lua -msgid "Any item belonging to the group(s)" -msgstr "Любой элемент из группы" - -#: init.lua -msgid "Cooking time" -msgstr "Время преготовления" - -#: init.lua -msgid "Burning time" -msgstr "Время горения" - -#: init.lua -msgid "Alternate" -msgstr "Другой" - -#: init.lua -msgid "Recipe @1 of @2" -msgstr "Рецепт @1 из @2" - -#: init.lua -msgid "" -"Recipe is too big to\n" -"be displayed (@1x@2)" -msgstr "" -"Рецепт слишком большой\n" -"для показа (@1x@2)" - -#: init.lua -msgid "Search" -msgstr "Поиск" - -#: init.lua -msgid "Reset" -msgstr "Сброс" - -#: init.lua -msgid "Increase window size" -msgstr "Увеличить окно" - -#: init.lua -msgid "Decrease window size" -msgstr "Уменьшить окно" - -#: init.lua -msgid "Previous page" -msgstr "Предыдущая страница" - -#: init.lua -msgid "Next page" -msgstr "Следущая страница" - -#: init.lua -msgid "No item to show" -msgstr "Нет элемента для показа" - -#: init.lua -msgid "Crafting Guide" -msgstr "книга рецептов крафта" - -#: init.lua -msgid "Crafting Guide Sign" -msgstr "табличка рецептов крафта" - -#: init.lua -msgid "Shows a list of available crafting recipes, cooking recipes and fuels" -msgstr "Показывает список рецептов крафта, преготовления и топливо" diff --git a/locale/template b/locale/template new file mode 100644 index 0000000..4edbd45 --- /dev/null +++ b/locale/template @@ -0,0 +1,28 @@ +# textdomain: craftguide + +Craft Guide= +Crafting Guide= +Crafting Guide Sign= +Usage @1 of @2= +Recipe @1 of @2= +No recipes= +No usages= +Burning time: @1= +Cooking time: @1= +Replaced by @1 on smelting= +Replaced by @1 on burning= +Replaced by @1 on crafting= +Repairable by step of @1= +Any item belonging to the group(s): @1= +Recipe's too big to be displayed (@1x@2)= +Shapeless= +Cooking= +No item to show= +Collect items to reveal more recipes= +Show recipe(s) of the pointed node= +No node pointed= +You don't know a recipe or usage for this item= +No recipe or usage for this item= +Digging= +Digging Chance= +@1 of chance to drop= diff --git a/locale/template.pot b/locale/template.pot deleted file mode 100644 index 8f8470d..0000000 --- a/locale/template.pot +++ /dev/null @@ -1,88 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-23 18:35+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" -"Content-Transfer-Encoding: 8bit\n" - -#: init.lua -msgid "Unknown Item (@1)" -msgstr "" - -#: init.lua -msgid "Any item belonging to the group(s)" -msgstr "" - -#: init.lua -msgid "Cooking time" -msgstr "" - -#: init.lua -msgid "Burning time" -msgstr "" - -#: init.lua -msgid "Alternate" -msgstr "" - -#: init.lua -msgid "Recipe @1 of @2" -msgstr "" - -#: init.lua -msgid "" -"Recipe is too big to\n" -"be displayed (@1x@2)" -msgstr "" - -#: init.lua -msgid "Search" -msgstr "" - -#: init.lua -msgid "Reset" -msgstr "" - -#: init.lua -msgid "Increase window size" -msgstr "" - -#: init.lua -msgid "Decrease window size" -msgstr "" - -#: init.lua -msgid "Previous page" -msgstr "" - -#: init.lua -msgid "Next page" -msgstr "" - -#: init.lua -msgid "No item to show" -msgstr "" - -#: init.lua -msgid "Crafting Guide" -msgstr "" - -#: init.lua -msgid "Crafting Guide Sign" -msgstr "" - -#: init.lua -msgid "Shows a list of available crafting recipes, cooking recipes and fuels" -msgstr "" diff --git a/mod.conf b/mod.conf index c32f714..cb6dbd9 100644 --- a/mod.conf +++ b/mod.conf @@ -1 +1,3 @@ name = craftguide +optional_depends = sfinv, sfinv_buttons +description = The most comprehensive Crafting Guide on Minetest diff --git a/settingtypes.txt b/settingtypes.txt index 48fab09..14198ae 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -1,4 +1,5 @@ -#For enabling some options of craftguide. - +# The progressive mode shows recipes you can craft from items you ever had in your inventory. craftguide_progressive_mode (Progressive Mode) bool false + +# Integration in the default Minetest Game inventory. craftguide_sfinv_only (Sfinv only) bool false \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000..860d318 --- /dev/null +++ b/test.json @@ -0,0 +1,8 @@ +{ + "items": [ + "default:stone, default:stone, default:stone", + "default:stone, , default:stone", + "default:stone, default:stone, default:stone" + ], + "result": "default:cobble 16" +} \ No newline at end of file diff --git a/textures/craftguide_bg.png b/textures/craftguide_bg.png index db50299..2d7d1fc 100644 Binary files a/textures/craftguide_bg.png and b/textures/craftguide_bg.png differ diff --git a/textures/craftguide_bg_full.png b/textures/craftguide_bg_full.png new file mode 100644 index 0000000..a3c41ba Binary files /dev/null and b/textures/craftguide_bg_full.png differ diff --git a/textures/craftguide_clear_icon_hover.png b/textures/craftguide_clear_icon_hover.png new file mode 100644 index 0000000..dccf373 Binary files /dev/null and b/textures/craftguide_clear_icon_hover.png differ diff --git a/textures/craftguide_next_icon_hover.png b/textures/craftguide_next_icon_hover.png new file mode 100644 index 0000000..449d3d2 Binary files /dev/null and b/textures/craftguide_next_icon_hover.png differ diff --git a/textures/craftguide_prev_icon.png b/textures/craftguide_prev_icon.png deleted file mode 100644 index b26cd15..0000000 Binary files a/textures/craftguide_prev_icon.png and /dev/null differ diff --git a/textures/craftguide_search_icon.png b/textures/craftguide_search_icon.png index aace804..4c4dd41 100644 Binary files a/textures/craftguide_search_icon.png and b/textures/craftguide_search_icon.png differ diff --git a/textures/craftguide_search_icon_hover.png b/textures/craftguide_search_icon_hover.png new file mode 100644 index 0000000..dd30668 Binary files /dev/null and b/textures/craftguide_search_icon_hover.png differ diff --git a/textures/craftguide_selected.png b/textures/craftguide_selected.png new file mode 100644 index 0000000..753589d Binary files /dev/null and b/textures/craftguide_selected.png differ diff --git a/textures/craftguide_sign.png b/textures/craftguide_sign.png index f64b5ab..2d1c4aa 100644 Binary files a/textures/craftguide_sign.png and b/textures/craftguide_sign.png differ diff --git a/textures/craftguide_sign_inv.png b/textures/craftguide_sign_inv.png deleted file mode 100644 index 6137234..0000000 Binary files a/textures/craftguide_sign_inv.png and /dev/null differ diff --git a/textures/craftguide_zoomin_icon.png b/textures/craftguide_zoomin_icon.png deleted file mode 100644 index 5b8ecc2..0000000 Binary files a/textures/craftguide_zoomin_icon.png and /dev/null differ diff --git a/textures/craftguide_zoomout_icon.png b/textures/craftguide_zoomout_icon.png deleted file mode 100644 index 7db747f..0000000 Binary files a/textures/craftguide_zoomout_icon.png and /dev/null differ