-- Crafting Mod - semi-realistic crafting in minetest -- Copyright (C) 2018 rubenwardy -- -- This library is free software; you can redistribute it and/or -- modify it under the terms of the GNU Lesser General Public -- License as published by the Free Software Foundation; either -- version 2.1 of the License, or (at your option) any later version. -- -- This library is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -- Lesser General Public License for more details. -- -- You should have received a copy of the GNU Lesser General Public -- License along with this library; if not, write to the Free Software -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA crafting = { recipes = {}, recipes_by_id = {}, registered_on_crafts = {}, } function crafting.register_type(name) crafting.recipes[name] = {} end local recipe_counter = 0 function crafting.register_recipe(def) assert(def.output, "Output needed in recipe definition") assert(def.type, "Type needed in recipe definition") assert(def.items, "Items needed in recipe definition") def.level = def.level or 1 local tab = crafting.recipes[def.type] assert(tab, "Unknown craft type " .. def.type) recipe_counter = recipe_counter + 1 def.id = recipe_counter crafting.recipes_by_id[recipe_counter] = def tab[#tab + 1] = def return def.id end local unlocked_cache = {} function crafting.get_unlocked(name) local player = minetest.get_player_by_name(name) if not player then minetest.log("warning", "Crafting doesn't support getting unlocks for offline players") return {} end local retval = unlocked_cache[name] if not retval then retval = minetest.parse_json( player:get_meta():get("crafting:unlocked") or "{}") unlocked_cache[name] = retval end assert(retval) return retval end if minetest then minetest.register_on_leaveplayer(function(player) unlocked_cache[player:get_player_name()] = nil end) end local function write_json_dictionary(value) if next(value) then return minetest.write_json(value) else return "{}" end end function crafting.lock_all(name) local player = minetest.get_player_by_name(name) if not player then minetest.log("warning", "Crafting doesn't support setting unlocks for offline players") return {} end local unlocked = crafting.get_unlocked(name) for key, _ in pairs(unlocked) do unlocked[key] = nil end unlocked_cache[name] = unlocked player:get_meta():set_string("crafting:unlocked", write_json_dictionary(unlocked)) end function crafting.unlock(name, output) local player = minetest.get_player_by_name(name) if not player then minetest.log("warning", "Crafting doesn't support setting unlocks for offline players") return {} end local unlocked = crafting.get_unlocked(name) if type(output) == "table" then for i=1, #output do unlocked[output[i]] = true minetest.chat_send_player(name, "You've unlocked " .. output[i]) end else unlocked[output] = true minetest.chat_send_player(name, "You've unlocked " .. output) end unlocked_cache[name] = unlocked player:get_meta():set_string("crafting:unlocked", write_json_dictionary(unlocked)) end function crafting.get_recipe(id) return crafting.recipes_by_id[id] end function crafting.get_all(type, level, item_hash, unlocked) assert(crafting.recipes[type], "No such craft type!") local results = {} for _, recipe in pairs(crafting.recipes[type]) do local craftable = true if recipe.level <= level and (recipe.always_known or unlocked[recipe.output]) then -- Check all ingredients are available local items = {} for _, item in pairs(recipe.items) do item = ItemStack(item) local needed_count = item:get_count() local available_count = item_hash[item:get_name()] or 0 if available_count < needed_count then craftable = false end items[#items + 1] = { name = item:get_name(), have = available_count, need = needed_count, } end results[#results + 1] = { recipe = recipe, items = items, craftable = craftable, } end end return results end function crafting.set_item_hashes_from_list(inv, listname, item_hash) for _, stack in pairs(inv:get_list(listname)) do if not stack:is_empty() then local itemname = stack:get_name() item_hash[itemname] = (item_hash[itemname] or 0) + stack:get_count() local def = minetest.registered_items[itemname] if def and def.groups then for groupname, _ in pairs(def.groups) do local group = "group:" .. groupname item_hash[group] = (item_hash[group] or 0) + stack:get_count() end end end end end function crafting.get_all_for_player(player, type, level) local unlocked = crafting.get_unlocked(player:get_player_name()) -- Get items hashed local item_hash = {} crafting.set_item_hashes_from_list(player:get_inventory(), "main", item_hash) return crafting.get_all(type, level, item_hash, unlocked) end function crafting.can_craft(name, type, level, recipe) local unlocked = crafting.get_unlocked(name) return recipe.type == type and recipe.level <= level and (recipe.always_known or unlocked[recipe.output]) end local function give_all_to_player(inv, list) for _, item in pairs(list) do inv:add_item("main", item) end end function crafting.find_required_items(inv, listname, recipe) local items = {} for _, item in pairs(recipe.items) do item = ItemStack(item) local itemname = item:get_name() if item:get_name():sub(1, 6) == "group:" then local groupname = itemname:sub(7, #itemname) local required = item:get_count() -- Find stacks in group for i = 1, inv:get_size(listname) do local stack = inv:get_stack(listname, i) -- Is it in group? local def = minetest.registered_items[stack:get_name()] if def and def.groups and def.groups[groupname] then stack = ItemStack(stack) if stack:get_count() > required then stack:set_count(required) end items[#items + 1] = stack required = required - stack:get_count() if required == 0 then break end end end if required > 0 then return nil end else if inv:contains_item(listname, item) then items[#items + 1] = item else return nil end end end return items end function crafting.has_required_items(inv, listname, recipe) return crafting.find_required_items(inv, listname, recipe) ~= nil end function crafting.register_on_craft(func) table.insert(crafting.registered_on_crafts, func) end function crafting.perform_craft(name, inv, listname, outlistname, recipe) local items = crafting.find_required_items(inv, listname, recipe) if not items then return false end -- Take items local taken = {} for _, item in pairs(items) do item = ItemStack(item) local took = inv:remove_item(listname, item) taken[#taken + 1] = took if took:get_count() ~= item:get_count() then minetest.log("error", "Unexpected lack of items in inventory") give_all_to_player(inv, taken) return false end end for i=1, #crafting.registered_on_crafts do crafting.registered_on_crafts[i](name, recipe) end -- Add output if inv:room_for_item("main", recipe.output) then inv:add_item(outlistname, recipe.output) else local pos = minetest.get_player_by_name(name):get_pos() minetest.chat_send_player(name, "No room in inventory!") minetest.add_item(pos, recipe.output) end return true end local function to_hex(str) return (str:gsub('.', function (c) return string.format('%02X', string.byte(c)) end)) end function crafting.calc_inventory_list_hash(inv, listname) local str = "" for _, stack in pairs(inv:get_list(listname)) do str = str .. stack:get_name() .. stack:get_count() end return minetest.sha1(to_hex(str)) end